From 747449d995a62d0eeaaa470a088410cfe0068a39 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Fri, 18 Oct 2024 00:30:06 +0100 Subject: [PATCH 01/20] refactor!: revamp Web UI --- .../Rules.Framework.WebUI.Sample/Program.cs | 48 +- .../Properties/launchSettings.json | 2 +- .../ReadmeExample/BasicRulesEngineExample.cs | 3 +- .../Rules.Framework.WebUI.Sample.csproj | 2 +- .../appsettings.Development.json | 13 +- src/Rules.Framework.WebUI/Assets/app.css | 135 +++ src/Rules.Framework.WebUI/Assets/engine.svg | 15 + src/Rules.Framework.WebUI/Assets/favicon.svg | 17 + src/Rules.Framework.WebUI/Assets/logo.svg | 17 + .../Components/Layout/MainLayout.razor | 35 + .../Components/Layout/NavMenu.razor | 60 ++ .../Components/Layout/NavMenu.razor.css | 102 ++ .../Components/Layout/SimpleLayout.razor | 34 + .../Components/Layout/SimpleLayout.razor.css | 0 .../PageComponents/ExportRules.razor | 137 +++ .../RuleConditionHierarchicalAccordion.razor | 170 ++++ .../Components/Pages/Instance.razor | 133 +++ .../Components/Pages/InstanceSelection.razor | 66 ++ .../Components/Pages/Rulesets.razor | 117 +++ .../Components/Pages/SearchRules.razor | 384 ++++++++ .../Components/Routes.razor | 7 + .../Components/WebUIApp.razor | 40 + .../Components/_Imports.razor | 18 + .../Dto/ComposedConditionNodeDto.cs | 9 - .../Dto/ConditionNodeDto.cs | 7 - .../Dto/IRuleStatusDtoAnalyzer.cs | 9 - src/Rules.Framework.WebUI/Dto/RuleDto.cs | 14 - .../Dto/RuleStatusDto.cs | 10 - .../Dto/RuleStatusDtoAnalyzer.cs | 27 - .../Dto/RulesFilterDto.cs | 14 - src/Rules.Framework.WebUI/Dto/RulesetDto.cs | 10 - .../Extensions/RuleDtoExtensions.cs | 59 -- .../Handlers/GetConfigurationsHandler.cs | 54 -- .../Handlers/GetIndexPageHandler.cs | 97 -- .../Handlers/GetRulesHandler.cs | 159 ---- .../Handlers/GetRulesetsHandler.cs | 67 -- src/Rules.Framework.WebUI/HttpMethod.cs | 12 - .../IHttpRequestHandler.cs | 10 - .../IRulesEngineInstancesRegistrar.cs | 27 + .../Rules.Framework.WebUI.csproj | 94 +- .../Services/GuidGenerator.cs | 20 + .../Services/IRulesEngineInstanceProvider.cs | 12 + .../Services/RulesEngineInstance.cs | 13 + .../Services/RulesEngineInstanceProvider.cs | 82 ++ .../PolymorphicWriteOnlyJsonConverter.cs | 4 +- .../ComposedConditionNodeViewModel.cs | 9 + .../ViewModels/ConditionNodeViewModel.cs | 7 + .../ViewModels/OptionViewModel.cs | 16 + .../ViewModels/RuleViewModel.cs | 27 + .../ViewModels/RuleViewModelExtensions.cs | 108 +++ .../RulesEngineInstanceViewModel.cs | 16 + .../ViewModels/RulesetViewModel.cs | 19 + .../ValueConditionNodeViewModel.cs} | 4 +- .../WebUIApplicationBuilderExtensions.cs | 103 +- src/Rules.Framework.WebUI/WebUIConstants.cs | 39 + src/Rules.Framework.WebUI/WebUIMiddleware.cs | 89 -- .../WebUIMvcBuilderExtensions.cs | 59 ++ src/Rules.Framework.WebUI/WebUIOptions.cs | 25 +- .../WebUIOptionsRegistry.cs | 19 + .../WebUIRequestHandlerBase.cs | 97 -- src/Rules.Framework.WebUI/index.html | 895 ------------------ .../bootstrap/css/bootstrap.min.css | 7 - .../bootstrap/dist/bootstrap.bundle.min.js | 7 - .../css/bootstrap.css | 819 ---------------- .../css/bootstrap.min.css | 14 - .../fonts/glyphicons-halflings-regular.eot | Bin 20127 -> 0 bytes .../fonts/glyphicons-halflings-regular.svg | 288 ------ .../fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes .../node_modules/jquery/dist/jquery.min.js | 2 - .../paginationjs/dist/pagination.css | 1 - .../paginationjs/dist/pagination.min.js | 11 - .../node_modules/rules_list.ico | Bin 6926 -> 0 bytes src/Rules.Framework.WebUI/package.json | 12 - .../Extensions/RuleDtoExtensionsTests.cs | 36 - .../Handlers/GetConfigurationsHandlerTests.cs | 64 -- .../Handlers/GetIndexPageHandlerTests.cs | 40 - .../Handlers/GetRulesHandlerTests.cs | 99 -- .../Handlers/GetRulesetsHandlerTests.cs | 62 -- .../Rules.Framework.WebUI.Tests.csproj | 30 +- .../Services/GuidGeneratorTests.cs | 49 + .../RulesEngineInstanceProviderTests.cs | 100 ++ .../Utilities/HttpContextHelper.cs | 69 -- .../Utilities/WebUIMiddlewareFactory.cs | 24 - .../RuleViewModelExtensionsTests.cs | 125 +++ .../WebUIMiddlewareTests.cs | 36 - .../WebUIOptionsRegistryTests.cs | 41 + 88 files changed, 2397 insertions(+), 3436 deletions(-) create mode 100644 src/Rules.Framework.WebUI/Assets/app.css create mode 100644 src/Rules.Framework.WebUI/Assets/engine.svg create mode 100644 src/Rules.Framework.WebUI/Assets/favicon.svg create mode 100644 src/Rules.Framework.WebUI/Assets/logo.svg create mode 100644 src/Rules.Framework.WebUI/Components/Layout/MainLayout.razor create mode 100644 src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor create mode 100644 src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor.css create mode 100644 src/Rules.Framework.WebUI/Components/Layout/SimpleLayout.razor create mode 100644 src/Rules.Framework.WebUI/Components/Layout/SimpleLayout.razor.css create mode 100644 src/Rules.Framework.WebUI/Components/PageComponents/ExportRules.razor create mode 100644 src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor create mode 100644 src/Rules.Framework.WebUI/Components/Pages/Instance.razor create mode 100644 src/Rules.Framework.WebUI/Components/Pages/InstanceSelection.razor create mode 100644 src/Rules.Framework.WebUI/Components/Pages/Rulesets.razor create mode 100644 src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor create mode 100644 src/Rules.Framework.WebUI/Components/Routes.razor create mode 100644 src/Rules.Framework.WebUI/Components/WebUIApp.razor create mode 100644 src/Rules.Framework.WebUI/Components/_Imports.razor delete mode 100644 src/Rules.Framework.WebUI/Dto/ComposedConditionNodeDto.cs delete mode 100644 src/Rules.Framework.WebUI/Dto/ConditionNodeDto.cs delete mode 100644 src/Rules.Framework.WebUI/Dto/IRuleStatusDtoAnalyzer.cs delete mode 100644 src/Rules.Framework.WebUI/Dto/RuleDto.cs delete mode 100644 src/Rules.Framework.WebUI/Dto/RuleStatusDto.cs delete mode 100644 src/Rules.Framework.WebUI/Dto/RuleStatusDtoAnalyzer.cs delete mode 100644 src/Rules.Framework.WebUI/Dto/RulesFilterDto.cs delete mode 100644 src/Rules.Framework.WebUI/Dto/RulesetDto.cs delete mode 100644 src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs delete mode 100644 src/Rules.Framework.WebUI/Handlers/GetConfigurationsHandler.cs delete mode 100644 src/Rules.Framework.WebUI/Handlers/GetIndexPageHandler.cs delete mode 100644 src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs delete mode 100644 src/Rules.Framework.WebUI/Handlers/GetRulesetsHandler.cs delete mode 100644 src/Rules.Framework.WebUI/HttpMethod.cs delete mode 100644 src/Rules.Framework.WebUI/IHttpRequestHandler.cs create mode 100644 src/Rules.Framework.WebUI/IRulesEngineInstancesRegistrar.cs create mode 100644 src/Rules.Framework.WebUI/Services/GuidGenerator.cs create mode 100644 src/Rules.Framework.WebUI/Services/IRulesEngineInstanceProvider.cs create mode 100644 src/Rules.Framework.WebUI/Services/RulesEngineInstance.cs create mode 100644 src/Rules.Framework.WebUI/Services/RulesEngineInstanceProvider.cs rename src/Rules.Framework.WebUI/{Utitlies => Utilities}/PolymorphicWriteOnlyJsonConverter.cs (84%) create mode 100644 src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs create mode 100644 src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs create mode 100644 src/Rules.Framework.WebUI/ViewModels/OptionViewModel.cs create mode 100644 src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs create mode 100644 src/Rules.Framework.WebUI/ViewModels/RuleViewModelExtensions.cs create mode 100644 src/Rules.Framework.WebUI/ViewModels/RulesEngineInstanceViewModel.cs create mode 100644 src/Rules.Framework.WebUI/ViewModels/RulesetViewModel.cs rename src/Rules.Framework.WebUI/{Dto/ValueConditionNodeDto.cs => ViewModels/ValueConditionNodeViewModel.cs} (65%) create mode 100644 src/Rules.Framework.WebUI/WebUIConstants.cs delete mode 100644 src/Rules.Framework.WebUI/WebUIMiddleware.cs create mode 100644 src/Rules.Framework.WebUI/WebUIMvcBuilderExtensions.cs create mode 100644 src/Rules.Framework.WebUI/WebUIOptionsRegistry.cs delete mode 100644 src/Rules.Framework.WebUI/WebUIRequestHandlerBase.cs delete mode 100644 src/Rules.Framework.WebUI/index.html delete mode 100644 src/Rules.Framework.WebUI/node_modules/bootstrap/css/bootstrap.min.css delete mode 100644 src/Rules.Framework.WebUI/node_modules/bootstrap/dist/bootstrap.bundle.min.js delete mode 100644 src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.css delete mode 100644 src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.min.css delete mode 100644 src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.eot delete mode 100644 src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.svg delete mode 100644 src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.ttf delete mode 100644 src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.woff delete mode 100644 src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.woff2 delete mode 100644 src/Rules.Framework.WebUI/node_modules/jquery/dist/jquery.min.js delete mode 100644 src/Rules.Framework.WebUI/node_modules/paginationjs/dist/pagination.css delete mode 100644 src/Rules.Framework.WebUI/node_modules/paginationjs/dist/pagination.min.js delete mode 100644 src/Rules.Framework.WebUI/node_modules/rules_list.ico delete mode 100644 src/Rules.Framework.WebUI/package.json delete mode 100644 tests/Rules.Framework.WebUI.Tests/Extensions/RuleDtoExtensionsTests.cs delete mode 100644 tests/Rules.Framework.WebUI.Tests/Handlers/GetConfigurationsHandlerTests.cs delete mode 100644 tests/Rules.Framework.WebUI.Tests/Handlers/GetIndexPageHandlerTests.cs delete mode 100644 tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesHandlerTests.cs delete mode 100644 tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesetsHandlerTests.cs create mode 100644 tests/Rules.Framework.WebUI.Tests/Services/GuidGeneratorTests.cs create mode 100644 tests/Rules.Framework.WebUI.Tests/Services/RulesEngineInstanceProviderTests.cs delete mode 100644 tests/Rules.Framework.WebUI.Tests/Utilities/HttpContextHelper.cs delete mode 100644 tests/Rules.Framework.WebUI.Tests/Utilities/WebUIMiddlewareFactory.cs create mode 100644 tests/Rules.Framework.WebUI.Tests/ViewModels/RuleViewModelExtensionsTests.cs delete mode 100644 tests/Rules.Framework.WebUI.Tests/WebUIMiddlewareTests.cs create mode 100644 tests/Rules.Framework.WebUI.Tests/WebUIOptionsRegistryTests.cs diff --git a/samples/Rules.Framework.WebUI.Sample/Program.cs b/samples/Rules.Framework.WebUI.Sample/Program.cs index 36e947dd..94ba3684 100644 --- a/samples/Rules.Framework.WebUI.Sample/Program.cs +++ b/samples/Rules.Framework.WebUI.Sample/Program.cs @@ -11,7 +11,22 @@ public static void Main(string[] args) var builder = WebApplication.CreateBuilder(args); // Add services to the container. - builder.Services.AddControllersWithViews(); + builder.Services.AddControllersWithViews() + .AddRulesFrameworkWebUI(registrar => + { + registrar.AddInstance("Readme example", (_, _) => new BasicRulesEngineExample().RulesEngine) + .AddInstance("Random rules example", async (_, _) => + { + var rulesProvider = new RulesEngineProvider(new RulesBuilder(new List() + { + new RulesRandomFactory() + })); + + return await rulesProvider.GetRulesEngineAsync(); + }); + }); + + builder.Logging.SetMinimumLevel(LogLevel.Trace).AddConsole(); var app = builder.Build(); @@ -24,15 +39,18 @@ public static void Main(string[] args) app.UseHsts(); } - app.UseHttpsRedirection(); - app.UseStaticFiles(); app.UseRouting(); + app.UseAntiforgery(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); - AddRulesFrameworkUI(app, useReadmeExample: false); + app.UseRulesFrameworkWebUI(opt => + { + opt.DocumentTitle = "Sample rules"; + }); app.MapControllerRoute( name: "default", @@ -40,27 +58,5 @@ public static void Main(string[] args) app.Run(); } - - private static void AddRulesFrameworkUI(IApplicationBuilder app, bool useReadmeExample = false) - { - if (useReadmeExample) - { - app.UseRulesFrameworkWebUI(new BasicRulesEngineExample().RulesEngine); - - return; - } - - var rulesProvider = new RulesEngineProvider(new RulesBuilder(new List() - { - new RulesRandomFactory() - })); - - var rulesEngine = rulesProvider - .GetRulesEngineAsync() - .GetAwaiter() - .GetResult(); - - app.UseRulesFrameworkWebUI(rulesEngine); - } } } \ No newline at end of file diff --git a/samples/Rules.Framework.WebUI.Sample/Properties/launchSettings.json b/samples/Rules.Framework.WebUI.Sample/Properties/launchSettings.json index 7f60a9ce..2d5650b7 100644 --- a/samples/Rules.Framework.WebUI.Sample/Properties/launchSettings.json +++ b/samples/Rules.Framework.WebUI.Sample/Properties/launchSettings.json @@ -12,7 +12,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "rules/index.html", + "launchUrl": "rules-ui/instance-selection", "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/samples/Rules.Framework.WebUI.Sample/ReadmeExample/BasicRulesEngineExample.cs b/samples/Rules.Framework.WebUI.Sample/ReadmeExample/BasicRulesEngineExample.cs index 38a8ae4f..fdcac8cb 100644 --- a/samples/Rules.Framework.WebUI.Sample/ReadmeExample/BasicRulesEngineExample.cs +++ b/samples/Rules.Framework.WebUI.Sample/ReadmeExample/BasicRulesEngineExample.cs @@ -2,7 +2,6 @@ namespace Rules.Framework.WebUI.Sample.ReadmeExample { using System; using global::Rules.Framework.WebUI.Sample.Engine; - using global::Rules.Framework.WebUI.Sample.Enums; internal class BasicRulesEngineExample { @@ -35,7 +34,7 @@ protected void AddRules(IEnumerable()) + foreach (var rulesetName in Enum.GetValues()) { this.RulesEngine.CreateRulesetAsync(rulesetName.ToString()) .GetAwaiter() diff --git a/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj b/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj index 2155439f..c6467180 100644 --- a/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj +++ b/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/samples/Rules.Framework.WebUI.Sample/appsettings.Development.json b/samples/Rules.Framework.WebUI.Sample/appsettings.Development.json index 0c208ae9..ce466063 100644 --- a/samples/Rules.Framework.WebUI.Sample/appsettings.Development.json +++ b/samples/Rules.Framework.WebUI.Sample/appsettings.Development.json @@ -1,8 +1,9 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } } - } -} +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Assets/app.css b/src/Rules.Framework.WebUI/Assets/app.css new file mode 100644 index 00000000..7266677e --- /dev/null +++ b/src/Rules.Framework.WebUI/Assets/app.css @@ -0,0 +1,135 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #006bb7; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + max-height: calc(100vh - 3.5rem); + height: calc(100vh - 3.5rem); +} + +.sidebar { + background-color: rgb(255, 255, 255, 100); +} + +.top-row { + height: 3.5rem; + background-color: rgb(255, 255, 255, 100); +} + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } + + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: calc(100vh - 3.5rem); + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row { + padding-left: 0 !important; + padding-right: 0 !important; + } +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Assets/engine.svg b/src/Rules.Framework.WebUI/Assets/engine.svg new file mode 100644 index 00000000..eaf54afe --- /dev/null +++ b/src/Rules.Framework.WebUI/Assets/engine.svg @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Assets/favicon.svg b/src/Rules.Framework.WebUI/Assets/favicon.svg new file mode 100644 index 00000000..dae39f05 --- /dev/null +++ b/src/Rules.Framework.WebUI/Assets/favicon.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Assets/logo.svg b/src/Rules.Framework.WebUI/Assets/logo.svg new file mode 100644 index 00000000..dae39f05 --- /dev/null +++ b/src/Rules.Framework.WebUI/Assets/logo.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Components/Layout/MainLayout.razor b/src/Rules.Framework.WebUI/Components/Layout/MainLayout.razor new file mode 100644 index 00000000..21d280d9 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Layout/MainLayout.razor @@ -0,0 +1,35 @@ +@attribute [ExcludeFromCodeCoverage] +@inherits LayoutComponentBase + +
+
+
+ +
+ Web UI +
+
+
+ + +
+
+ @Body +
+
+
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
\ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor b/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor new file mode 100644 index 00000000..3aeb3428 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor @@ -0,0 +1,60 @@ +@attribute [ExcludeFromCodeCoverage] +@rendermode InteractiveServer +@inject NavigationManager NavigationManager +@inject ProtectedSessionStorage Storage + + + + + +@code { + private bool shouldRenderSwitchInstanceLink = true; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var instanceIdResult = await Storage.GetAsync(WebUIConstants.SelectedInstanceStorageKey); + if (!instanceIdResult.Success || instanceIdResult.Value == Guid.Empty) + { + this.NavigationManager.NavigateTo("/rules-ui/instance-selection"); + } + + var isUniqueInstanceResult = await this.Storage.GetAsync(WebUIConstants.IsUniqueInstanceStorageKey); + if (isUniqueInstanceResult.Success && isUniqueInstanceResult.Value) + { + shouldRenderSwitchInstanceLink = false; + this.StateHasChanged(); + } + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor.css b/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor.css new file mode 100644 index 00000000..a9566a33 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor.css @@ -0,0 +1,102 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + + .navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); + } + +.top-row { + height: 3.5rem; + background-color: rgb(255, 255, 255, 100); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: rgb(0, 0, 0); + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + + .nav-item ::deep a.active { + background-color: rgba(0,0,0,0.37); + color: white; + } + + .nav-item ::deep .nav-link:hover { + background-color: rgba(0,0,0,0.1); + color: white; + } + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + display: block; + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Components/Layout/SimpleLayout.razor b/src/Rules.Framework.WebUI/Components/Layout/SimpleLayout.razor new file mode 100644 index 00000000..c9e650d3 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Layout/SimpleLayout.razor @@ -0,0 +1,34 @@ +@attribute [ExcludeFromCodeCoverage] +@inherits LayoutComponentBase + +
+
+
+ +
+ Web UI +
+
+
+ + +
+
+ @Body +
+
+
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/src/Rules.Framework.WebUI/Components/Layout/SimpleLayout.razor.css b/src/Rules.Framework.WebUI/Components/Layout/SimpleLayout.razor.css new file mode 100644 index 00000000..e69de29b diff --git a/src/Rules.Framework.WebUI/Components/PageComponents/ExportRules.razor b/src/Rules.Framework.WebUI/Components/PageComponents/ExportRules.razor new file mode 100644 index 00000000..8fd10728 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/PageComponents/ExportRules.razor @@ -0,0 +1,137 @@ +@attribute [ExcludeFromCodeCoverage] +@using System.Text.Json +@using System.Text +@using System.IO +@using System.Text.Json.Serialization +@rendermode InteractiveServer +@inject IJSRuntime JS + +
+
+        
+            @exportText
+        
+    
+
+ +
+ +
+ + + + .json + + @if (this.CloseButtonEnable) + { + + } +
+ +@code { + private bool customizeFileNameEnabled; + private string exportText = string.Empty; + private JsonSerializerOptions jsonSerializerOptions; + + public ExportRules() + { + this.Reset(); + } + + protected override void OnInitialized() + { + if (this.CloseButtonEnable && !this.CloseButtonCallback.HasDelegate) + { + throw new InvalidOperationException("Close button needs a callback when enabled."); + } + } + + protected override void OnAfterRender(bool firstRender) + { + this.exportText = JsonSerializer.Serialize(this.Rules, this.jsonSerializerOptions); + this.StateHasChanged(); + } + + + [Parameter] + public EventCallback CloseButtonCallback { get; set; } + + [Parameter] + public bool CloseButtonEnable { get; set; } + + [Parameter] + public string ExportFileName { get; set; } + + [Parameter] + public IEnumerable Rules { get; set; } + + private async Task OnExportModalCloseButtonClickAsync(MouseEventArgs e) + { + await this.CloseButtonCallback.InvokeAsync(); + this.Reset(); + } + + private async Task OnExportModalDownloadButtonClickAsync(MouseEventArgs e) + { + if (this.Rules != null && this.Rules.Any()) + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(exportText)); + using var streamReference = new DotNetStreamReference(stream); + await JS.InvokeVoidAsync("downloadFileFromStream", $"{this.ExportFileName}.json", streamReference); + } + } + + private void Reset() + { + this.exportText = string.Empty; + this.customizeFileNameEnabled = false; + this.jsonSerializerOptions = new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + WriteIndented = true, + }; + this.jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + this.jsonSerializerOptions.Converters.Add(new PolymorphicWriteOnlyJsonConverter()); + } + + public sealed class Rule + { + public bool Active { get; set; } + + public object Content { get; set; } + + public DateTime DateBegin { get; set; } + + public DateTime? DateEnd { get; set; } + + public string Name { get; set; } + + public int Priority { get; set; } + + public ConditionNode RootCondition { get; set; } + + public string Ruleset { get; set; } + } + + public sealed class ComposedConditionNode : ConditionNode + { + public IEnumerable ChildConditionNodes { get; set; } + } + + public class ConditionNode + { + public string LogicalOperator { get; set; } + } + + public sealed class ValueConditionNode : ConditionNode + { + public string Condition { get; set; } + + public string DataType { get; set; } + + public dynamic Operand { get; set; } + + public string Operator { get; set; } + } +} diff --git a/src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor b/src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor new file mode 100644 index 00000000..917aa739 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor @@ -0,0 +1,170 @@ +@attribute [ExcludeFromCodeCoverage] +@using System.Collections +@using System.Text; +@rendermode InteractiveAuto + +
+ @if (this.EnableShowAllButton) + { + + } + @if (this.EnableCollapseAllButton) + { + + } +
+
+ + @foreach (var conditionNode in this.ConditionNodes) + { + if (conditionNode is ComposedConditionNode composedConditionNode) + { + + + + + + } + else if (conditionNode is ValueConditionNode valueConditionNode) + { + var shortValueConditionText = $"{valueConditionNode.Condition} {valueConditionNode.Operator} {valueConditionNode.GetOperandPrettyPrint()}"; + + +
+
+ + + +
+
+

Condition name:

@(valueConditionNode.Condition)

+
+
+

Data type:

@(valueConditionNode.DataType)

+
+
+

Operator:

@(valueConditionNode.Operator)

+
+
+

Operand:

@(valueConditionNode.GetOperandPrettyPrint())

+
+
+
+
+
+
+
+
+
+ } + } +
+
+ +@code { + private Accordion innerAccordion; + private List innerRuleConditionHierarchicalAccordions = new List(); + private RuleConditionHierarchicalAccordion InnerReference + { + set + { + this.innerRuleConditionHierarchicalAccordions.Add(value); + } + } + + [Parameter] + public IEnumerable ConditionNodes { get; set; } = Enumerable.Empty(); + + [Parameter] + public bool EnableCollapseAllButton { get; set; } = false; + + [Parameter] + public bool EnableShowAllButton { get; set; } = false; + + public async Task CollapseAllAsync() + { + await this.innerAccordion.HideAllAccordionItemsAsync(); + if (this.innerRuleConditionHierarchicalAccordions.Count > 0) + { + foreach (var ruleConditionHierarchicalAccordion in this.innerRuleConditionHierarchicalAccordions) + { + await ruleConditionHierarchicalAccordion.CollapseAllAsync(); + } + } + } + + public async Task ShowAllAsync() + { + await this.innerAccordion.ShowAllAccordionItemsAsync(); + if (this.innerRuleConditionHierarchicalAccordions.Count > 0) + { + foreach (var ruleConditionHierarchicalAccordion in this.innerRuleConditionHierarchicalAccordions) + { + await ruleConditionHierarchicalAccordion.ShowAllAsync(); + } + } + } + + public sealed class ComposedConditionNode : ConditionNode + { + public IEnumerable ChildConditionNodes { get; internal set; } + } + + public class ConditionNode + { + public string LogicalOperator { get; internal set; } + } + + public sealed class ValueConditionNode : ConditionNode + { + public string Condition { get; internal set; } + + public string DataType { get; internal set; } + + public dynamic Operand { get; internal set; } + + public string Operator { get; internal set; } + + internal string GetOperandPrettyPrint() + { + var operandPrettyPrintBuilder = new StringBuilder(); + if (this.Operand is IEnumerable elements && elements is not string) + { + operandPrettyPrintBuilder.Append("{ "); + var elementsProcessedCount = 0; + foreach (var element in elements) + { + if (elementsProcessedCount >= 10) + { + operandPrettyPrintBuilder.Append(", ..."); + } + else if (elementsProcessedCount > 0) + { + operandPrettyPrintBuilder.Append(','); + } + + operandPrettyPrintBuilder.Append(element switch + { + null => "(null)", + "" => "(empty)", + _ => element, + }); + elementsProcessedCount++; + } + + operandPrettyPrintBuilder.Append(" }"); + } + else + { + operandPrettyPrintBuilder.Append(this.Operand switch + { + null => "(null)", + "" => "(empty)", + _ => this.Operand, + }); + } + + return operandPrettyPrintBuilder.ToString(); + } + } +} diff --git a/src/Rules.Framework.WebUI/Components/Pages/Instance.razor b/src/Rules.Framework.WebUI/Components/Pages/Instance.razor new file mode 100644 index 00000000..29ff90a4 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Pages/Instance.razor @@ -0,0 +1,133 @@ +@attribute [ExcludeFromCodeCoverage] +@page "/rules-ui/instance" +@rendermode InteractiveServer +@inject WebUIOptions Options +@inject ProtectedSessionStorage Storage +@inject IRulesEngineInstanceProvider RulesEngineInstanceProvider + +@(this.Options.DocumentTitle) - Instance + +

Instance

+ +@if (this.instanceId == Guid.Empty) +{ +

Loading... Please wait.

+} +else +{ +

Name: @(instance.Name)

+ +

Options

+ +
    + @foreach (var option in instance.Options) + { +
  • + @(option.Name) + @if (!string.IsNullOrEmpty(option.NameDescription)) + { + + + + + } + @if (string.Equals(option.Name, "DataTypeDefaults")) + { +
      + @foreach (var dataTypeDefault in (IDictionary)option.Value) + { +
    • @(dataTypeDefault.Key) @(dataTypeDefault.Value)
    • + } +
    + } + else + { + + + + @if (!string.IsNullOrEmpty(option.ValueDescription)) + { + + @(option.Value) + + } + else + { + @(option.Value) + } + } +
  • + } +
+} + + +@code { + private Guid instanceId; + private RulesEngineInstanceViewModel instance; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var instanceIdResult = await Storage.GetAsync(WebUIConstants.SelectedInstanceStorageKey); + if (instanceIdResult.Success) + { + this.instanceId = instanceIdResult.Value; + var instance = this.RulesEngineInstanceProvider.GetInstance(instanceId); + this.instance = new RulesEngineInstanceViewModel + { + Id = instance.Id, + Name = instance.Name, + Options = new List + { + new OptionViewModel + { + Name = nameof(instance.RulesEngine.Options.AutoCreateRulesets), + NameDescription = WebUIConstants.OptionAutoCreateRulesetsDescription, + Value = instance.RulesEngine.Options.AutoCreateRulesets.ToString().ToLower(), + }, + new OptionViewModel + { + Name = nameof(instance.RulesEngine.Options.DataTypeDefaults), + NameDescription = WebUIConstants.OptionDataTypeDefaultsDescription, + Value = instance.RulesEngine.Options.DataTypeDefaults.ToDictionary(k => k.Key.ToString(), v =>v.Key switch + { + DataTypes.Boolean => v.Value.ToString().ToLower(), + DataTypes.String when string.Equals(v.Value, string.Empty) => "string.Empty", + DataTypes.String => $"\"{v.Value}\"", + _ => v.Value, + }), + }, + new OptionViewModel + { + Name = nameof(instance.RulesEngine.Options.MissingConditionBehavior), + NameDescription = WebUIConstants.OptionMissingConditionBehaviorDescription, + Value = instance.RulesEngine.Options.MissingConditionBehavior.ToString(), + ValueDescription = instance.RulesEngine.Options.MissingConditionBehavior switch + { + MissingConditionBehaviors.UseDataTypeDefault => WebUIConstants.OptionMissingConditionBehaviorUseDataTypeDefaultDescription, + MissingConditionBehaviors.Discard => WebUIConstants.OptionMissingConditionBehaviorDiscardDescription, + _ => "", + }, + }, + new OptionViewModel + { + Name = nameof(instance.RulesEngine.Options.PriorityCriteria), + NameDescription = WebUIConstants.OptionPriorityCriteriaDescription, + Value = instance.RulesEngine.Options.PriorityCriteria.ToString(), + ValueDescription = instance.RulesEngine.Options.PriorityCriteria switch + { + PriorityCriterias.TopmostRuleWins => WebUIConstants.OptionPriorityCriteriaTopmostRuleWinsDescription, + PriorityCriterias.BottommostRuleWins => WebUIConstants.OptionPriorityCriteriaBottommostRuleWinsDescription, + _ => "", + }, + }, + }, + }; + + this.StateHasChanged(); + } + } + } +} diff --git a/src/Rules.Framework.WebUI/Components/Pages/InstanceSelection.razor b/src/Rules.Framework.WebUI/Components/Pages/InstanceSelection.razor new file mode 100644 index 00000000..7e04d972 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Pages/InstanceSelection.razor @@ -0,0 +1,66 @@ +@attribute [ExcludeFromCodeCoverage] +@page "/rules-ui/instance-selection" +@rendermode InteractiveServer +@layout global::Rules.Framework.WebUI.Components.Layout.SimpleLayout +@inject WebUIOptions Options +@inject IRulesEngineInstanceProvider RulesEngineInstanceProvider +@inject ProtectedSessionStorage Storage +@inject NavigationManager NavigationManager + +@(this.Options.DocumentTitle) - Select instance + +

Select a rules engine instance

+ +
+ @if (this.allInstances is not null) + { + foreach (var instance in this.allInstances) + { +
+ + +

@(instance.Name)

+ +
+
+
+ } + } +
+ +@code { + private IEnumerable allInstances; + private string selectedInstanceId; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && this.allInstances.Count() == 1) + { + var instance = this.allInstances.First(); + await this.Storage.SetAsync(WebUIConstants.SelectedInstanceStorageKey, instance.Id); + await this.Storage.SetAsync(WebUIConstants.IsUniqueInstanceStorageKey, true); + await this.Storage.DeleteAsync(WebUIConstants.SelectedRulesetsStorageKey); + this.NavigationManager.NavigateTo("rules-ui/instance"); + } + } + + protected override void OnInitialized() + { + this.allInstances = this.RulesEngineInstanceProvider.GetAllInstances(); + } + + private async Task SelectInstanceAsync(Guid instanceId) + { + if (instanceId != Guid.Empty) + { + var instance = this.RulesEngineInstanceProvider.GetInstance(instanceId); + if (instance is not null) + { + await this.Storage.SetAsync(WebUIConstants.SelectedInstanceStorageKey, instanceId); + await this.Storage.SetAsync(WebUIConstants.IsUniqueInstanceStorageKey, false); + await this.Storage.DeleteAsync(WebUIConstants.SelectedRulesetsStorageKey); + this.NavigationManager.NavigateTo("rules-ui/instance"); + } + } + } +} diff --git a/src/Rules.Framework.WebUI/Components/Pages/Rulesets.razor b/src/Rules.Framework.WebUI/Components/Pages/Rulesets.razor new file mode 100644 index 00000000..67f8f30b --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Pages/Rulesets.razor @@ -0,0 +1,117 @@ +@attribute [ExcludeFromCodeCoverage] +@page "/rules-ui/rulesets" +@rendermode InteractiveServer +@inject WebUIOptions Options +@inject ProtectedSessionStorage Storage +@inject NavigationManager NavigationManager +@inject IRulesEngineInstanceProvider RulesEngineInstanceProvider + +@(this.Options.DocumentTitle) - Rulesets + +

Rulesets

+ +
+ @if (this.instanceId == Guid.Empty) + { +
+ + +
+ } + else + { + + + + @(context.Number) + + + @(context.Name) + + + @(context.ActiveRulesCount) + + + @(context.TotalRulesCount) + + + + + + + } +
+ +@code { + private const int DefaultPageSize = 5; + private int currentPageNumber = 1; + private int totalPages = 0; + private Guid instanceId; + private List allRulesets; + private IEnumerable filteredRulesets; + private IEnumerable pagedRulesets; + private bool onlyShowRulesetsWithRules = false; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var instanceIdResult = await this.Storage.GetAsync(WebUIConstants.SelectedInstanceStorageKey); + if (instanceIdResult.Success) + { + this.instanceId = instanceIdResult.Value; + this.StateHasChanged(); + } + } + } + + private async Task> LoadRulesetsAsync(GridDataProviderRequest request) + { + var instance = this.RulesEngineInstanceProvider.GetInstance(instanceId); + var rulesets = await instance.RulesEngine.GetRulesetsAsync(); + var number = 1; + var rulesetViewModels = rulesets.OrderBy(r => r.Name).Select(r => new RulesetViewModel + { + Id = GuidGenerator.GenerateFromString(r.Name), + Name = r.Name, + Number = number++, + }).ToList(); + + foreach (var ruleset in rulesetViewModels) + { + var rules = await instance.RulesEngine.SearchAsync(new SearchArgs( + ruleset.Name, + DateTime.MinValue, + DateTime.MaxValue)); + + ruleset.TotalRulesCount = rules.Count(); + ruleset.ActiveRulesCount = rules.Count(r => r.Active); + } + + return request.ApplyTo(rulesetViewModels); + } + + private async Task ButtonViewRulesOnClickAsync(Guid rulesetId) + { + await this.Storage.SetAsync(WebUIConstants.SelectedRulesetsStorageKey, new[] { rulesetId }); + + this.NavigationManager.NavigateTo($"rules-ui/search-rules"); + } +} diff --git a/src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor b/src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor new file mode 100644 index 00000000..4a8b568f --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor @@ -0,0 +1,384 @@ +@attribute [ExcludeFromCodeCoverage] +@page "/rules-ui/search-rules" +@rendermode InteractiveServer +@using System.Text.Json +@using System.Text.Json.Serialization +@using System.IO +@using System.Text +@inject WebUIOptions Options +@inject IJSRuntime JS +@inject NavigationManager NavigationManager +@inject IRulesEngineInstanceProvider RulesEngineInstanceProvider +@inject ProtectedSessionStorage Storage + +@(this.Options.DocumentTitle) - Search rules + +

Search rules

+ +
+
+
+
+
+ + Select rulesets + + @if (this.instanceId == Guid.Empty) + { +

Loading...

+ } + else + { +
+ + +
+ + foreach (var ruleset in this.rulesets) + { +
+ + +
+ } + } +
+
+
+
+
+ @if (this.instanceId != Guid.Empty && this.rulesetIds.Any()) + { + @(this.rulesetIds.Count) selected: + var count = 0; + foreach (var ruleset in this.rulesets) + { + if (this.rulesetIds.Contains(ruleset.Id)) + { + if (count > 0) + { + , + } + + if (count == 5) + { + and more... + break; + } + + @(ruleset.Name) + count++; + } + } + } +
+
+
+
+
+
+
Rules
+
+
+ @priorityCriteria + + Actions + + Export JSON + + +
+
+
+ + + + @(context.Priority) + + + @(context.Ruleset) + + + @(context.Name) + + + @(context.DateBegin) + + + @(context.DateEnd) + + + @(JsonSerializer.Serialize(context.Content, this.jsonSerializerOptions)) + + + @if (context.Active) + { + Active + } + else + { + Deactivated + } + + + + + + +
+
+                                            
+                                                @(JsonSerializer.Serialize(context.RootCondition, this.jsonSerializerOptions))
+                                            
+                                        
+
+
+
+ + +
+ @if (context.RootCondition is not null) + { + var conditionNodes = new[] { ConvertToComponentModel(context.RootCondition) }; + + } +
+
+
+
+
+
+
+
+
+
+ + + +@code { + private bool allRulesetsSelected; + private const int DefaultPageSize = 5; + private DateTime? dateBegin; + private DateInput dateBeginSearch; + private DateTime? dateEnd; + private Guid instanceId; + private JsonSerializerOptions jsonSerializerOptions; + private string exportFileName; + private Modal exportModal; + private string exportText; + private string priorityCriteria; + private string priorityCriteriaTooltip; + private HashSet rulesetIds; + private List rulesets; + private Grid rulesGrid; + + public SearchRules() + { + this.jsonSerializerOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + WriteIndented = true, + }; + this.jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + this.jsonSerializerOptions.Converters.Add(new PolymorphicWriteOnlyJsonConverter()); + this.rulesetIds = new HashSet(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var instanceIdResult = await this.Storage.GetAsync(WebUIConstants.SelectedInstanceStorageKey); + if (instanceIdResult.Success) + { + this.instanceId = instanceIdResult.Value; + var instance = this.RulesEngineInstanceProvider.GetInstance(this.instanceId); + + switch (instance.RulesEngine.Options.PriorityCriteria) + { + case PriorityCriterias.BottommostRuleWins: + this.priorityCriteria = WebUIConstants.OptionPriorityCriteriaBottommostRuleWinsName; + this.priorityCriteriaTooltip = WebUIConstants.OptionPriorityCriteriaBottommostRuleWinsDescription; + break; + default: + this.priorityCriteria = WebUIConstants.OptionPriorityCriteriaTopmostRuleWinsName; + this.priorityCriteriaTooltip = WebUIConstants.OptionPriorityCriteriaTopmostRuleWinsDescription; + break; + } + + var rulesets = await instance.RulesEngine.GetRulesetsAsync(); + var number = 1; + this.rulesets = rulesets.OrderBy(r => r.Name).Select(r => new RulesetViewModel + { + Id = GuidGenerator.GenerateFromString(r.Name), + Name = r.Name, + Number = number++, + }).ToList(); + + foreach (var ruleset in this.rulesets) + { + var rules = await instance.RulesEngine.SearchAsync(new SearchArgs( + ruleset.Name, + DateTime.MinValue, + DateTime.MaxValue)); + + ruleset.TotalRulesCount = rules.Count(); + ruleset.ActiveRulesCount = rules.Count(r => r.Active); + } + } + + var rulesetIdResult = await this.Storage.GetAsync(WebUIConstants.SelectedRulesetsStorageKey); + if (rulesetIdResult.Success) + { + this.rulesetIds = new HashSet(rulesetIdResult.Value); + } + + this.StateHasChanged(); + await this.rulesGrid.RefreshDataAsync(); + } + } + + private RuleConditionHierarchicalAccordion.ConditionNode ConvertToComponentModel(ConditionNodeViewModel viewModel) + { + return viewModel switch + { + ComposedConditionNodeViewModel composedConditionNodeViewModel => new RuleConditionHierarchicalAccordion.ComposedConditionNode + { + ChildConditionNodes = composedConditionNodeViewModel.ChildConditionNodes.Select(x => ConvertToComponentModel(x)), + LogicalOperator = composedConditionNodeViewModel.LogicalOperator, + }, + ValueConditionNodeViewModel valueConditionNodeViewModel => new RuleConditionHierarchicalAccordion.ValueConditionNode + { + Condition = valueConditionNodeViewModel.Condition, + DataType = valueConditionNodeViewModel.DataType, + LogicalOperator = valueConditionNodeViewModel.LogicalOperator, + Operand = valueConditionNodeViewModel.Operand, + Operator = valueConditionNodeViewModel.Operator, + }, + _ => throw new NotSupportedException(), + }; + } + + private async Task> LoadRulesAsync(GridDataProviderRequest request) + { + List ruleViewModels = new List(); + if (this.instanceId != Guid.Empty && this.rulesetIds.Any()) + { + + var instance = this.RulesEngineInstanceProvider.GetInstance(instanceId); + var rulesets = await instance.RulesEngine.GetRulesetsAsync(); + var rulesetsToSearch = this.rulesetIds.Contains(Guid.Empty) + ? rulesets + : rulesets.Where(r => this.rulesetIds.Contains(GuidGenerator.GenerateFromString(r.Name))); + + var searchDateBegin = this.dateBegin.GetValueOrDefault(DateTime.MinValue); + var searchDateEnd = this.dateEnd.GetValueOrDefault(DateTime.MaxValue); + foreach (var ruleset in rulesetsToSearch) + { + var rules = await instance.RulesEngine.SearchAsync(new SearchArgs(ruleset.Name, searchDateBegin, searchDateEnd)); + + ruleViewModels.AddRange(rules.OrderBy(r => r.Priority).Select(r => r.ToViewModel())); + } + } + + return request.ApplyTo(ruleViewModels); + } + + private async Task OnRulesetCheckAsync(Guid rulesetId) + { + if (rulesetId == Guid.Empty) + { + if (this.allRulesetsSelected) + { + this.rulesetIds.Clear(); + } + else + { + this.rulesetIds.Clear(); + foreach (var ruleset in this.rulesets) + { + this.rulesetIds.Add(ruleset.Id); + } + } + + this.allRulesetsSelected = !this.allRulesetsSelected; + } + else + { + if (this.rulesetIds.Contains(rulesetId)) + { + this.rulesetIds.Remove(rulesetId); + } + else + { + this.rulesetIds.Add(rulesetId); + } + } + + await this.Storage.SetAsync(WebUIConstants.SelectedRulesetsStorageKey, this.rulesetIds); + this.StateHasChanged(); + await this.rulesGrid.RefreshDataAsync(); + } + + private async Task OnExportJsonButtonClickAsync() + { + if (this.rulesetIds.Any()) + { + var request = new GridDataProviderRequest + { + Filters = this.rulesGrid.GetFilters(), + }; + var response = await this.rulesGrid.DataProvider.Invoke(request); + var rules = response.Data.Select(r => r.ToExportRulesModel()).ToArray(); + if (this.rulesetIds.Count > 1) + { + this.exportFileName = "selection-rules"; + } + else + { + var rulesetId = this.rulesetIds.Single(); + var selectedRulesetName = this.rulesets.First(r => r.Id == rulesetId).Name; + this.exportFileName = $"{selectedRulesetName}-selection-rules"; + } + + this.StateHasChanged(); + await this.exportModal.ShowAsync( + title: "Export result", + parameters: new Dictionary(StringComparer.Ordinal) + { + { "CloseButtonCallback", EventCallback.Factory.Create(this.exportModal, async () => await this.exportModal.HideAsync()) }, + { "CloseButtonEnable", true }, + { "ExportFileName", this.exportFileName }, + { "Rules", rules }, + }); + } + } + + +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Components/Routes.razor b/src/Rules.Framework.WebUI/Components/Routes.razor new file mode 100644 index 00000000..877c9eb1 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/Routes.razor @@ -0,0 +1,7 @@ +@attribute [ExcludeFromCodeCoverage] + + + + + + diff --git a/src/Rules.Framework.WebUI/Components/WebUIApp.razor b/src/Rules.Framework.WebUI/Components/WebUIApp.razor new file mode 100644 index 00000000..5d1aff5b --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/WebUIApp.razor @@ -0,0 +1,40 @@ +@attribute [ExcludeFromCodeCoverage] + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Rules.Framework.WebUI/Components/_Imports.razor b/src/Rules.Framework.WebUI/Components/_Imports.razor new file mode 100644 index 00000000..e22b18bb --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/_Imports.razor @@ -0,0 +1,18 @@ +@using System.Collections.Generic +@using System.Diagnostics.CodeAnalysis +@using System.Net.Http +@using System.Net.Http.Json +@using BlazorBootstrap +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using global::Rules.Framework.WebUI +@using global::Rules.Framework.WebUI.Components +@using global::Rules.Framework.WebUI.Components.PageComponents +@using global::Rules.Framework.WebUI.Services +@using global::Rules.Framework.WebUI.Utilities +@using global::Rules.Framework.WebUI.ViewModels diff --git a/src/Rules.Framework.WebUI/Dto/ComposedConditionNodeDto.cs b/src/Rules.Framework.WebUI/Dto/ComposedConditionNodeDto.cs deleted file mode 100644 index 6ba1cf97..00000000 --- a/src/Rules.Framework.WebUI/Dto/ComposedConditionNodeDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - using System.Collections.Generic; - - internal sealed class ComposedConditionNodeDto : ConditionNodeDto - { - public IEnumerable ChildConditionNodes { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/ConditionNodeDto.cs b/src/Rules.Framework.WebUI/Dto/ConditionNodeDto.cs deleted file mode 100644 index 8c37502d..00000000 --- a/src/Rules.Framework.WebUI/Dto/ConditionNodeDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - internal class ConditionNodeDto - { - public string LogicalOperator { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/IRuleStatusDtoAnalyzer.cs b/src/Rules.Framework.WebUI/Dto/IRuleStatusDtoAnalyzer.cs deleted file mode 100644 index d41e1587..00000000 --- a/src/Rules.Framework.WebUI/Dto/IRuleStatusDtoAnalyzer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - using System; - - internal interface IRuleStatusDtoAnalyzer - { - RuleStatusDto Analyze(DateTime dateBegin, DateTime? dateEnd); - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RuleDto.cs b/src/Rules.Framework.WebUI/Dto/RuleDto.cs deleted file mode 100644 index a122b80b..00000000 --- a/src/Rules.Framework.WebUI/Dto/RuleDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - internal sealed class RuleDto - { - public ConditionNodeDto Conditions { get; internal set; } - public string DateBegin { get; internal set; } - public string DateEnd { get; internal set; } - public string Name { get; internal set; } - public int? Priority { get; internal set; } - public string Ruleset { get; internal set; } - public string Status { get; internal set; } - public object Value { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RuleStatusDto.cs b/src/Rules.Framework.WebUI/Dto/RuleStatusDto.cs deleted file mode 100644 index a0c2cc5f..00000000 --- a/src/Rules.Framework.WebUI/Dto/RuleStatusDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - internal enum RuleStatusDto : short - { - Expired, - Active, - Pending, - Deactivated - } -} diff --git a/src/Rules.Framework.WebUI/Dto/RuleStatusDtoAnalyzer.cs b/src/Rules.Framework.WebUI/Dto/RuleStatusDtoAnalyzer.cs deleted file mode 100644 index 40e1d2b2..00000000 --- a/src/Rules.Framework.WebUI/Dto/RuleStatusDtoAnalyzer.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - using System; - - internal sealed class RuleStatusDtoAnalyzer : IRuleStatusDtoAnalyzer - { - public RuleStatusDto Analyze(DateTime dateBegin, DateTime? dateEnd) - { - if (dateBegin > DateTime.UtcNow) - { - return RuleStatusDto.Pending; - } - - if (!dateEnd.HasValue) - { - return RuleStatusDto.Active; - } - - if (dateEnd.Value <= DateTime.UtcNow) - { - return RuleStatusDto.Expired; - } - - return RuleStatusDto.Active; - } - } -} diff --git a/src/Rules.Framework.WebUI/Dto/RulesFilterDto.cs b/src/Rules.Framework.WebUI/Dto/RulesFilterDto.cs deleted file mode 100644 index 98fbed29..00000000 --- a/src/Rules.Framework.WebUI/Dto/RulesFilterDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - using System; - - internal sealed class RulesFilterDto - { - public string Content { get; set; } - public DateTime? DateBegin { get; set; } - public DateTime? DateEnd { get; set; } - public string Name { get; set; } - public string Ruleset { get; set; } - public RuleStatusDto? Status { get; set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RulesetDto.cs b/src/Rules.Framework.WebUI/Dto/RulesetDto.cs deleted file mode 100644 index d92ea633..00000000 --- a/src/Rules.Framework.WebUI/Dto/RulesetDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Rules.Framework.WebUI.Dto -{ - internal sealed class RulesetDto - { - public int Index { get; internal set; } - public int ActiveRulesCount { get; internal set; } - public string Name { get; internal set; } - public int RulesCount { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs b/src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs deleted file mode 100644 index 0bfabeb3..00000000 --- a/src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Rules.Framework.WebUI.Extensions -{ - using System.Collections.Generic; - using System.Linq; - using Rules.Framework.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 IConditionNode rootCondition) - { - if (rootCondition.LogicalOperator == Framework.LogicalOperators.Eval || - rootCondition.LogicalOperator == 0) - { - var condition = rootCondition as ValueConditionNode; - - return new ValueConditionNodeDto - { - Condition = condition.Condition, - DataType = condition.DataType.ToString(), - Operand = condition.Operand, - Operator = condition.Operator.ToString(), - }; - } - - var composedConditionNode = rootCondition as ComposedConditionNode; - - var conditionNodeDataModels = new List(composedConditionNode.ChildConditionNodes.Count()); - - foreach (var child in composedConditionNode.ChildConditionNodes) - { - conditionNodeDataModels.Add(child.ToConditionNodeDto()); - } - - return new ComposedConditionNodeDto - { - ChildConditionNodes = conditionNodeDataModels, - LogicalOperator = composedConditionNode.LogicalOperator.ToString() - }; - } - - public static RuleDto ToRuleDto(this Rule rule, IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer) - { - return new RuleDto - { - Conditions = rule.RootCondition?.ToConditionNodeDto(), - DateEnd = !rule.DateEnd.HasValue ? null : rule.DateEnd.Value.ToString(dateFormat), - DateBegin = rule.DateBegin.ToString(dateFormat), - Name = rule.Name, - Priority = rule.Priority, - Ruleset = rule.Ruleset, - Status = !rule.Active ? RuleStatusDto.Deactivated.ToString() : ruleStatusDtoAnalyzer.Analyze(rule.DateBegin, rule.DateEnd).ToString(), - Value = rule.ContentContainer.GetContentAs(), - }; - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Handlers/GetConfigurationsHandler.cs b/src/Rules.Framework.WebUI/Handlers/GetConfigurationsHandler.cs deleted file mode 100644 index 6ac4a0ce..00000000 --- a/src/Rules.Framework.WebUI/Handlers/GetConfigurationsHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Rules.Framework.WebUI.Handlers -{ - using System; - using System.Collections.Generic; - using System.Net; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - - internal sealed class GetConfigurationsHandler : WebUIRequestHandlerBase - { - private static readonly string[] resourcePath = new[] { "/{0}/api/v1/configurations" }; - - private readonly IRulesEngine rulesEngine; - - public GetConfigurationsHandler(IRulesEngine rulesEngine, WebUIOptions webUIOptions) : base(resourcePath, webUIOptions) - { - this.rulesEngine = rulesEngine; - } - - protected override HttpMethod HttpMethod => HttpMethod.GET; - - protected override Task HandleRequestAsync(HttpRequest httpRequest, HttpResponse httpResponse, RequestDelegate next) - { - try - { - var configurations = new Dictionary - { - { "PriorityCriteria", this.rulesEngine.Options.PriorityCriteria.ToString() }, - { "MissingConditionBehavior", this.rulesEngine.Options.MissingConditionBehavior.ToString() }, - { - "DataTypeDefaults", - new Dictionary - { - { "ArrayBoolean", this.rulesEngine.Options.DataTypeDefaults[DataTypes.ArrayBoolean].ToString() }, - { "ArrayDecimal", this.rulesEngine.Options.DataTypeDefaults[DataTypes.ArrayDecimal].ToString() }, - { "ArrayInteger", this.rulesEngine.Options.DataTypeDefaults[DataTypes.ArrayInteger].ToString() }, - { "ArrayString", this.rulesEngine.Options.DataTypeDefaults[DataTypes.ArrayString].ToString() }, - { "Boolean", this.rulesEngine.Options.DataTypeDefaults[DataTypes.Boolean].ToString() }, - { "Decimal", this.rulesEngine.Options.DataTypeDefaults[DataTypes.Decimal].ToString() }, - { "Integer", this.rulesEngine.Options.DataTypeDefaults[DataTypes.Integer].ToString() }, - { "String", this.rulesEngine.Options.DataTypeDefaults[DataTypes.String].ToString() }, - } - } - }; - - return this.WriteResponseAsync(httpResponse, configurations, (int)HttpStatusCode.OK); - } - catch (Exception ex) - { - return this.WriteExceptionResponseAsync(httpResponse, ex); - } - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Handlers/GetIndexPageHandler.cs b/src/Rules.Framework.WebUI/Handlers/GetIndexPageHandler.cs deleted file mode 100644 index 04734c5a..00000000 --- a/src/Rules.Framework.WebUI/Handlers/GetIndexPageHandler.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace Rules.Framework.WebUI.Handlers -{ - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text; - using System.Text.RegularExpressions; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - - internal sealed class GetIndexPageHandler : WebUIRequestHandlerBase - { - private static readonly string[] resourcePath = new[] { "/{0}", "/{0}/", "/{0}/index.html" }; - - public GetIndexPageHandler(WebUIOptions webUIOptions) : base(resourcePath, webUIOptions) - { - } - - protected override HttpMethod HttpMethod => HttpMethod.GET; - - protected override async Task HandleRequestAsync(HttpRequest httpRequest, HttpResponse httpResponse, RequestDelegate next) - { - var path = httpRequest.Path.Value; - var httpContext = httpRequest.HttpContext; - - if (Regex.IsMatch(path, $"^/?{Regex.Escape(this.WebUIOptions.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) - { - // Use relative redirect to support proxy environments - var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") - ? "index.html" - : $"{path.Split('/').Last()}/index.html"; - - RespondWithRedirect(httpContext.Response, relativeIndexUrl); - } - - if (Regex.IsMatch(path, $"^/{Regex.Escape(this.WebUIOptions.RoutePrefix)}/?index.html$", RegexOptions.IgnoreCase)) - { - await this.RespondWithIndexHtmlAsync(httpContext.Response, next).ConfigureAwait(false); - } - } - - private static void RespondWithRedirect(HttpResponse httpResponse, string location) - { - if (!httpResponse.HasStarted) - { - httpResponse.StatusCode = 301; - httpResponse.Headers["Location"] = location; - } - } - - private IDictionary GetIndexArguments() - { - return new Dictionary - { - { "%(DocumentTitle)", this.WebUIOptions.DocumentTitle }, - { "%(HeadContent)", this.WebUIOptions.HeadContent } - }; - } - - private async Task RespondWithIndexHtmlAsync(HttpResponse httpResponse, RequestDelegate next) - { - if (!httpResponse.HasStarted) - { - httpResponse.StatusCode = 200; - httpResponse.ContentType = "text/html;charset=utf-8"; - - var originalBody = httpResponse.Body; - - using (var stream = this.WebUIOptions.IndexStream()) - { - httpResponse.Body = stream; - await next(httpResponse.HttpContext).ConfigureAwait(false); - - using (var reader = new StreamReader(stream)) - { - var body = await reader.ReadToEndAsync().ConfigureAwait(false); - - var responseTextBuilder = new StringBuilder(body); - - foreach (var entry in this.GetIndexArguments()) - { - responseTextBuilder.Replace(entry.Key, entry.Value); - } - - var byteArray = Encoding.UTF8.GetBytes(responseTextBuilder.ToString()); - using (var newStream = new MemoryStream(byteArray)) - { - httpResponse.Body = originalBody; - newStream.Seek(0, SeekOrigin.Begin); - await newStream.CopyToAsync(httpResponse.Body).ConfigureAwait(false); - } - } - } - } - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs b/src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs deleted file mode 100644 index 4999cbc7..00000000 --- a/src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs +++ /dev/null @@ -1,159 +0,0 @@ -namespace Rules.Framework.WebUI.Handlers -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Text.Json; - using System.Threading.Tasks; - using System.Web; - using Microsoft.AspNetCore.Http; - using Rules.Framework.WebUI.Dto; - using Rules.Framework.WebUI.Extensions; - - internal sealed class GetRulesHandler : WebUIRequestHandlerBase - { - private static readonly string[] resourcePath = new[] { "/{0}/api/v1/rules" }; - private readonly IRulesEngine rulesEngine; - private readonly IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer; - - public GetRulesHandler(IRulesEngine rulesEngine, IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer, WebUIOptions webUIOptions) : base(resourcePath, webUIOptions) - { - this.rulesEngine = rulesEngine; - this.ruleStatusDtoAnalyzer = ruleStatusDtoAnalyzer; - } - - protected override HttpMethod HttpMethod => HttpMethod.GET; - - protected override async Task HandleRequestAsync(HttpRequest httpRequest, - HttpResponse httpResponse, - RequestDelegate next) - { - var rulesFilter = this.GetRulesFilterFromRequest(httpRequest); - - if (!IsValidFilterDates(rulesFilter)) - { - await this.WriteResponseAsync(httpResponse, new { Message = "Date begin cannot be greater than after" }, (int)HttpStatusCode.BadRequest) - .ConfigureAwait(false); - - return; - } - - try - { - var rules = new List(); - - if (rulesFilter.Ruleset.Equals("all")) - { - var rulesets = await this.rulesEngine.GetRulesetsAsync(); - - foreach (var ruleset in rulesets) - { - var rulesForRuleset = await this.GetRulesForRuleset(ruleset.Name, rulesFilter).ConfigureAwait(false); - rules.AddRange(rulesForRuleset); - } - } - else - { - var rulesForRuleset = await this.GetRulesForRuleset(rulesFilter.Ruleset, rulesFilter).ConfigureAwait(false); - rules.AddRange(rulesForRuleset); - } - - await this.WriteResponseAsync(httpResponse, rules, (int)HttpStatusCode.OK).ConfigureAwait(false); - } - catch (Exception ex) - { - await this.WriteExceptionResponseAsync(httpResponse, ex).ConfigureAwait(false); - } - } - - private static bool IsValidFilterDates(RulesFilterDto rulesFilter) - { - return (rulesFilter.DateBegin is null - || rulesFilter.DateEnd is null) || - (rulesFilter.DateBegin <= rulesFilter.DateEnd); - } - - private IEnumerable ApplyFilters(RulesFilterDto rulesFilter, IEnumerable genericRulesDto) - { - if (!string.IsNullOrWhiteSpace(rulesFilter.Content)) - { - genericRulesDto = genericRulesDto.Where(g => - { -#if NETSTANDARD2_0 - return JsonSerializer.Serialize(g.Value).ToUpper().Contains(rulesFilter.Content.ToUpper()); -#else - return JsonSerializer.Serialize(g.Value).Contains(rulesFilter.Content, StringComparison.OrdinalIgnoreCase); -#endif - }); - } - - if (!string.IsNullOrWhiteSpace(rulesFilter.Name)) - { - genericRulesDto = genericRulesDto.Where(g => - { -#if NETSTANDARD2_0 - return g.Name.ToUpper().Contains(rulesFilter.Name.ToUpper()); -#else - return g.Name.Contains(rulesFilter.Name, StringComparison.OrdinalIgnoreCase); -#endif - }); - } - if (rulesFilter.Status != null) - { - genericRulesDto = genericRulesDto.Where(g => - { - return g.Status.Equals(rulesFilter.Status.ToString()); - }); - } - - return genericRulesDto; - } - - private RulesFilterDto GetRulesFilterFromRequest(HttpRequest httpRequest) - { - var parseQueryString = HttpUtility.ParseQueryString(httpRequest.QueryString.Value); - - var rulesFilterAsString = JsonSerializer.Serialize(parseQueryString.Cast().ToDictionary(k => k, v => string.IsNullOrWhiteSpace(parseQueryString[v]) ? null : parseQueryString[v])); - var rulesFilter = JsonSerializer.Deserialize(rulesFilterAsString, this.SerializerOptions); - - rulesFilter.Ruleset = string.IsNullOrWhiteSpace(rulesFilter.Ruleset) ? "all" : rulesFilter.Ruleset; - - rulesFilter.DateEnd ??= DateTime.MaxValue; - - rulesFilter.DateBegin ??= DateTime.MinValue; - - return rulesFilter; - } - - private async Task> GetRulesForRuleset(string ruleset, RulesFilterDto rulesFilter) - { - var genericRules = await this.rulesEngine.SearchAsync( - new SearchArgs( - ruleset, - rulesFilter.DateBegin.Value, - rulesFilter.DateEnd.Value)) - .ConfigureAwait(false); - - var priorityCriteria = this.rulesEngine.Options.PriorityCriteria; - - if (genericRules != null && genericRules.Any()) - { - if (priorityCriteria == PriorityCriterias.BottommostRuleWins) - { - genericRules = genericRules.OrderByDescending(r => r.Priority); - } - else - { - genericRules = genericRules.OrderBy(r => r.Priority); - } - - var genericRulesDto = this.ApplyFilters(rulesFilter, genericRules.Select(g => g.ToRuleDto(this.ruleStatusDtoAnalyzer))); - - return genericRulesDto; - } - - return Enumerable.Empty(); - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Handlers/GetRulesetsHandler.cs b/src/Rules.Framework.WebUI/Handlers/GetRulesetsHandler.cs deleted file mode 100644 index dbd1bd6a..00000000 --- a/src/Rules.Framework.WebUI/Handlers/GetRulesetsHandler.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace Rules.Framework.WebUI.Handlers -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Rules.Framework.WebUI.Dto; - - internal sealed class GetRulesetsHandler : WebUIRequestHandlerBase - { - private static readonly string[] resourcePath = new[] { "/{0}/api/v1/rulesets" }; - - private readonly IRulesEngine rulesEngine; - private readonly IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer; - - public GetRulesetsHandler(IRulesEngine rulesEngine, - IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer, - WebUIOptions webUIOptions) : base(resourcePath, webUIOptions) - { - this.rulesEngine = rulesEngine; - this.ruleStatusDtoAnalyzer = ruleStatusDtoAnalyzer; - } - - protected override HttpMethod HttpMethod => HttpMethod.GET; - - protected override async Task HandleRequestAsync(HttpRequest httpRequest, HttpResponse httpResponse, RequestDelegate next) - { - try - { - var rulesets = await this.rulesEngine.GetRulesetsAsync().ConfigureAwait(false); - - var rulesetDtos = new List(); - var index = 0; - foreach (var ruleset in rulesets) - { - var rules = await this.rulesEngine - .SearchAsync(new SearchArgs(ruleset.Name, - DateTime.MinValue, - DateTime.MaxValue)) - .ConfigureAwait(false); - - rulesetDtos.Add(new RulesetDto - { - Index = index, - Name = ruleset.Name, - ActiveRulesCount = rules.Count(IsActive), - RulesCount = rules.Count() - }); - index++; - } - - await this.WriteResponseAsync(httpResponse, rulesetDtos, (int)HttpStatusCode.OK).ConfigureAwait(false); - } - catch (Exception ex) - { - await this.WriteExceptionResponseAsync(httpResponse, ex).ConfigureAwait(false); - } - } - - private bool IsActive(Rule rule) - { - return this.ruleStatusDtoAnalyzer.Analyze(rule.DateBegin, rule.DateEnd) == RuleStatusDto.Active; - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/HttpMethod.cs b/src/Rules.Framework.WebUI/HttpMethod.cs deleted file mode 100644 index 164e6808..00000000 --- a/src/Rules.Framework.WebUI/HttpMethod.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Rules.Framework.WebUI -{ - internal enum HttpMethod - { - None = 0, - GET = 1, - POST = 2, - PUT = 3, - PATCH = 4, - DELETE = 5 - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/IHttpRequestHandler.cs b/src/Rules.Framework.WebUI/IHttpRequestHandler.cs deleted file mode 100644 index 996f5c8c..00000000 --- a/src/Rules.Framework.WebUI/IHttpRequestHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Rules.Framework.WebUI -{ - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - - internal interface IHttpRequestHandler - { - Task HandleAsync(HttpRequest httpRequest, HttpResponse httpResponse, RequestDelegate next); - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/IRulesEngineInstancesRegistrar.cs b/src/Rules.Framework.WebUI/IRulesEngineInstancesRegistrar.cs new file mode 100644 index 00000000..7483fbd7 --- /dev/null +++ b/src/Rules.Framework.WebUI/IRulesEngineInstancesRegistrar.cs @@ -0,0 +1,27 @@ +namespace Rules.Framework.WebUI +{ + using System; + using System.Threading.Tasks; + + /// + /// Represents the registration of instances on the Web UI. + /// + public interface IRulesEngineInstancesRegistrar + { + /// + /// Adds a rules engine instance to be presented and used on Web UI. + /// + /// The name. + /// The get rules engine function. + /// + IRulesEngineInstancesRegistrar AddInstance(string name, Func getRulesEngineFunc); + + /// + /// Adds a rules engine instance to be presented and used on Web UI. + /// + /// The name. + /// The get rules engine function. + /// + IRulesEngineInstancesRegistrar AddInstance(string name, Func> getRulesEngineFunc); + } +} \ 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 adc3426e..572ea993 100644 --- a/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj +++ b/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj @@ -1,75 +1,47 @@ - - - - true - true - net6.0;netstandard2.0 - 10.0 - - - - - - - LICENSE.md - - - Git - rules rulesframework web ui - A rules framework web ui that allows you to see the rules configured in your application - - + + + true + true + net8.0 + 10.0 + + + + + + + LICENSE.md + + + Git + rules rulesframework web ui + A rules framework web ui that allows you to see the rules configured in your application + + True + - - - - - - - - - - - - + + + + $(AssemblyName).Assets.$(AssemblyName).styles.css + + + - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - + - + + + - - - - - - - - - + \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Services/GuidGenerator.cs b/src/Rules.Framework.WebUI/Services/GuidGenerator.cs new file mode 100644 index 00000000..3c9dbda7 --- /dev/null +++ b/src/Rules.Framework.WebUI/Services/GuidGenerator.cs @@ -0,0 +1,20 @@ +namespace Rules.Framework.WebUI.Services +{ + using System; + using System.Security.Cryptography; + using System.Text; + + internal static class GuidGenerator + { + private static readonly HashAlgorithm hashAlgorithm = SHA256.Create(); + + public static Guid GenerateFromString(string source) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentException.ThrowIfNullOrWhiteSpace(source); + + var hashedBytes = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(source)); + return new Guid(hashedBytes[..16]); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Services/IRulesEngineInstanceProvider.cs b/src/Rules.Framework.WebUI/Services/IRulesEngineInstanceProvider.cs new file mode 100644 index 00000000..7427f626 --- /dev/null +++ b/src/Rules.Framework.WebUI/Services/IRulesEngineInstanceProvider.cs @@ -0,0 +1,12 @@ +namespace Rules.Framework.WebUI.Services +{ + using System; + using System.Collections.Generic; + + internal interface IRulesEngineInstanceProvider + { + IEnumerable GetAllInstances(); + + RulesEngineInstance GetInstance(Guid instanceId); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Services/RulesEngineInstance.cs b/src/Rules.Framework.WebUI/Services/RulesEngineInstance.cs new file mode 100644 index 00000000..52db89cd --- /dev/null +++ b/src/Rules.Framework.WebUI/Services/RulesEngineInstance.cs @@ -0,0 +1,13 @@ +namespace Rules.Framework.WebUI.Services +{ + using System; + + internal class RulesEngineInstance + { + public Guid Id { get; set; } + + public string Name { get; set; } + + public IRulesEngine RulesEngine { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Services/RulesEngineInstanceProvider.cs b/src/Rules.Framework.WebUI/Services/RulesEngineInstanceProvider.cs new file mode 100644 index 00000000..fba78c49 --- /dev/null +++ b/src/Rules.Framework.WebUI/Services/RulesEngineInstanceProvider.cs @@ -0,0 +1,82 @@ +namespace Rules.Framework.WebUI.Services +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + internal sealed class RulesEngineInstanceProvider : IRulesEngineInstanceProvider, IRulesEngineInstancesRegistrar + { + private readonly List<(Guid, string, Func)> instanceMappings; + private readonly Dictionary instances; + private bool instancesEnumerated; + + public RulesEngineInstanceProvider() + { + this.instanceMappings = new List<(Guid, string, Func)>(); + this.instances = new Dictionary(); + this.instancesEnumerated = false; + } + + public IRulesEngineInstancesRegistrar AddInstance(string name, Func getRulesEngineFunc) + { + if (this.instanceMappings.Any(m => string.Equals(m.Item2, name, StringComparison.Ordinal))) + { + throw new InvalidOperationException($"A rules engine instance with name '{name}' has already been specified."); + } + + this.instanceMappings.Add((GuidGenerator.GenerateFromString(name), name, getRulesEngineFunc)); + return this; + } + + public IRulesEngineInstancesRegistrar AddInstance(string name, Func> getRulesEngineFunc) + => this.AddInstance(name, (serviceProvider, name) => getRulesEngineFunc.Invoke(serviceProvider, name).GetAwaiter().GetResult()); + + public void EnumerateInstances(IServiceProvider serviceProvider) + { + if (!instancesEnumerated) + { + foreach (var (id, name, getRulesEngineFunc) in instanceMappings) + { + var instance = new RulesEngineInstance + { + Id = id, + Name = name, + RulesEngine = getRulesEngineFunc.Invoke(serviceProvider, name), + }; + + this.instances.Add(id, instance); + } + + instancesEnumerated = true; + } + } + + public IEnumerable GetAllInstances() + { + if (!instancesEnumerated) + { + throw new InvalidOperationException($"Instances enumeration is required before invoking '{nameof(GetAllInstances)}'." + + $" Please ensure '{nameof(EnumerateInstances)}' is invoked first."); + } + + return this.instances.Values; + } + + public RulesEngineInstance GetInstance(Guid instanceId) + { + if (!instancesEnumerated) + { + throw new InvalidOperationException($"Instances enumeration is required before invoking '{nameof(GetInstance)}'." + + $" Please ensure '{nameof(EnumerateInstances)}' is invoked first."); + } + + if (this.instances.TryGetValue(instanceId, out var instance)) + { + return instance; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Utitlies/PolymorphicWriteOnlyJsonConverter.cs b/src/Rules.Framework.WebUI/Utilities/PolymorphicWriteOnlyJsonConverter.cs similarity index 84% rename from src/Rules.Framework.WebUI/Utitlies/PolymorphicWriteOnlyJsonConverter.cs rename to src/Rules.Framework.WebUI/Utilities/PolymorphicWriteOnlyJsonConverter.cs index 35dc6bba..186eedf5 100644 --- a/src/Rules.Framework.WebUI/Utitlies/PolymorphicWriteOnlyJsonConverter.cs +++ b/src/Rules.Framework.WebUI/Utilities/PolymorphicWriteOnlyJsonConverter.cs @@ -1,9 +1,11 @@ -namespace Rules.Framework.WebUI.Utitlies +namespace Rules.Framework.WebUI.Utilities { using System; + using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; + [ExcludeFromCodeCoverage] internal sealed class PolymorphicWriteOnlyJsonConverter : JsonConverter { public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs new file mode 100644 index 00000000..ef46e62e --- /dev/null +++ b/src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.WebUI.ViewModels +{ + using System.Collections.Generic; + + internal sealed class ComposedConditionNodeViewModel : ConditionNodeViewModel + { + public IEnumerable ChildConditionNodes { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs new file mode 100644 index 00000000..e1c1ec46 --- /dev/null +++ b/src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs @@ -0,0 +1,7 @@ +namespace Rules.Framework.WebUI.ViewModels +{ + internal class ConditionNodeViewModel + { + public string LogicalOperator { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/OptionViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/OptionViewModel.cs new file mode 100644 index 00000000..16bdbe92 --- /dev/null +++ b/src/Rules.Framework.WebUI/ViewModels/OptionViewModel.cs @@ -0,0 +1,16 @@ +namespace Rules.Framework.WebUI.ViewModels +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal sealed class OptionViewModel + { + public string Name { get; set; } + + public string NameDescription { get; set; } + + public object Value { get; set; } + + public string ValueDescription { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs new file mode 100644 index 00000000..ac3b6c59 --- /dev/null +++ b/src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs @@ -0,0 +1,27 @@ +namespace Rules.Framework.WebUI.ViewModels +{ + using System; + + internal sealed class RuleViewModel + { + public bool Active { get; set; } + + public string Conditions { get; set; } + + public object Content { get; set; } + + public DateTime DateBegin { get; set; } + + public DateTime? DateEnd { get; set; } + + public Guid Id { get; set; } + + public string Name { get; set; } + + public int Priority { get; set; } + + public ConditionNodeViewModel RootCondition { get; set; } + + public string Ruleset { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/RuleViewModelExtensions.cs b/src/Rules.Framework.WebUI/ViewModels/RuleViewModelExtensions.cs new file mode 100644 index 00000000..d68ad47c --- /dev/null +++ b/src/Rules.Framework.WebUI/ViewModels/RuleViewModelExtensions.cs @@ -0,0 +1,108 @@ +namespace Rules.Framework.WebUI.ViewModels +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Rules.Framework.ConditionNodes; + using Rules.Framework.WebUI.Components.PageComponents; + using Rules.Framework.WebUI.Services; + + internal static class RuleViewModelExtensions + { + public static ExportRules.Rule ToExportRulesModel(this RuleViewModel ruleViewModel) + { + return new ExportRules.Rule + { + Active = ruleViewModel.Active, + Content = ruleViewModel.Content, + DateBegin = ruleViewModel.DateBegin, + DateEnd = ruleViewModel.DateEnd, + Name = ruleViewModel.Name, + Priority = ruleViewModel.Priority, + RootCondition = ruleViewModel.RootCondition.ToExportRulesModel(), + Ruleset = ruleViewModel.Ruleset, + }; + } + + public static RuleViewModel ToViewModel(this Rule rule) + { + return new RuleViewModel + { + Active = rule.Active, + Content = rule.ContentContainer.GetContentAs(), + DateBegin = rule.DateBegin, + DateEnd = rule.DateEnd, + Id = GuidGenerator.GenerateFromString(rule.Name), + Name = rule.Name, + Priority = rule.Priority, + RootCondition = rule.RootCondition.ToViewModel(), + Ruleset = rule.Ruleset, + }; + } + + private static ExportRules.ConditionNode ToExportRulesModel(this ConditionNodeViewModel rootCondition) + { + if (rootCondition.LogicalOperator == nameof(LogicalOperators.Eval)) + { + var condition = rootCondition as ValueConditionNodeViewModel; + + return new ExportRules.ValueConditionNode + { + Condition = condition.Condition, + DataType = condition.DataType.ToString(), + LogicalOperator = condition.LogicalOperator, + Operand = condition.Operand, + Operator = condition.Operator.ToString(), + }; + } + + var composedConditionNode = rootCondition as ComposedConditionNodeViewModel; + + var conditionNodeDataModels = new List(composedConditionNode.ChildConditionNodes.Count()); + + foreach (var child in composedConditionNode.ChildConditionNodes) + { + conditionNodeDataModels.Add(child.ToExportRulesModel()); + } + + return new ExportRules.ComposedConditionNode + { + ChildConditionNodes = conditionNodeDataModels, + LogicalOperator = composedConditionNode.LogicalOperator.ToString(), + }; + } + + private static ConditionNodeViewModel ToViewModel(this IConditionNode rootCondition) + { + if (rootCondition.LogicalOperator == LogicalOperators.Eval || + rootCondition.LogicalOperator == 0) + { + var condition = rootCondition as ValueConditionNode; + + return new ValueConditionNodeViewModel + { + Condition = condition.Condition, + DataType = condition.DataType.ToString(), + LogicalOperator = condition.LogicalOperator.ToString(), + Operand = condition.Operand, + Operator = condition.Operator.ToString(), + }; + } + + var composedConditionNode = rootCondition as ComposedConditionNode; + + var conditionNodeDataModels = new List(composedConditionNode.ChildConditionNodes.Count()); + + foreach (var child in composedConditionNode.ChildConditionNodes) + { + conditionNodeDataModels.Add(child.ToViewModel()); + } + + return new ComposedConditionNodeViewModel + { + ChildConditionNodes = conditionNodeDataModels, + LogicalOperator = composedConditionNode.LogicalOperator.ToString(), + }; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/RulesEngineInstanceViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/RulesEngineInstanceViewModel.cs new file mode 100644 index 00000000..5c5d43ac --- /dev/null +++ b/src/Rules.Framework.WebUI/ViewModels/RulesEngineInstanceViewModel.cs @@ -0,0 +1,16 @@ +namespace Rules.Framework.WebUI.ViewModels +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal sealed class RulesEngineInstanceViewModel + { + public Guid Id { get; set; } + + public string Name { get; set; } + + public IEnumerable Options { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/RulesetViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/RulesetViewModel.cs new file mode 100644 index 00000000..68efb6fe --- /dev/null +++ b/src/Rules.Framework.WebUI/ViewModels/RulesetViewModel.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.WebUI.ViewModels +{ + using System; + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal sealed class RulesetViewModel + { + public int ActiveRulesCount { get; set; } + + public Guid Id { get; set; } + + public string Name { get; set; } + + public int Number { get; set; } + + public int TotalRulesCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/ValueConditionNodeDto.cs b/src/Rules.Framework.WebUI/ViewModels/ValueConditionNodeViewModel.cs similarity index 65% rename from src/Rules.Framework.WebUI/Dto/ValueConditionNodeDto.cs rename to src/Rules.Framework.WebUI/ViewModels/ValueConditionNodeViewModel.cs index 46d67539..db128258 100644 --- a/src/Rules.Framework.WebUI/Dto/ValueConditionNodeDto.cs +++ b/src/Rules.Framework.WebUI/ViewModels/ValueConditionNodeViewModel.cs @@ -1,6 +1,6 @@ -namespace Rules.Framework.WebUI.Dto +namespace Rules.Framework.WebUI.ViewModels { - internal sealed class ValueConditionNodeDto : ConditionNodeDto + internal sealed class ValueConditionNodeViewModel : ConditionNodeViewModel { public string Condition { get; internal set; } diff --git a/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs b/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs index 1cbc5455..0eb15f33 100644 --- a/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs +++ b/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs @@ -1,106 +1,71 @@ namespace Rules.Framework.WebUI { using System; - using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Threading.Tasks; + using Components; using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Options; - using Rules.Framework.WebUI.Dto; - using Rules.Framework.WebUI.Handlers; + using Microsoft.Extensions.FileProviders; + using Rules.Framework.WebUI.Services; /// /// extension for Rules Framework Web UI /// + [ExcludeFromCodeCoverage] public static class WebUIApplicationBuilderExtensions { /// /// Uses the rules framework web UI. /// /// The application. - /// The rules engine factory. - /// The web UI options action. /// public static IApplicationBuilder UseRulesFrameworkWebUI( - this IApplicationBuilder app, - Func rulesEngineFactory, - Action webUIOptionsAction) - { - var genericRulesEngine = rulesEngineFactory.Invoke(app.ApplicationServices); - return app.UseRulesFrameworkWebUI(genericRulesEngine, webUIOptionsAction); - } - - /// - /// Uses the rules framework web UI. - /// - /// The application. - /// The rules engine factory. - /// - public static IApplicationBuilder UseRulesFrameworkWebUI( - this IApplicationBuilder app, - Func rulesEngineFactory) - { - return app.UseRulesFrameworkWebUI(rulesEngineFactory, null); - } - - /// - /// Uses the rules framework web UI. - /// - /// The application. - /// The rules engine. - /// - public static IApplicationBuilder UseRulesFrameworkWebUI( - this IApplicationBuilder app, - IRulesEngine rulesEngine) + this IApplicationBuilder app) { - return app.UseRulesFrameworkWebUI(rulesEngine, new WebUIOptions()); + return app.UseRulesFrameworkWebUI(options => { }); } /// /// Uses the rules framework web UI. /// /// The application. - /// The rules engine. - /// The web UI options action. + /// The web UI options configuration action. /// public static IApplicationBuilder UseRulesFrameworkWebUI( this IApplicationBuilder app, - IRulesEngine rulesEngine, Action webUIOptionsAction) { - WebUIOptions webUIOptions; + var rulesEngineInstanceProvider = app.ApplicationServices.GetRequiredService(); + rulesEngineInstanceProvider.EnumerateInstances(app.ApplicationServices); - using (var scope = app.ApplicationServices.CreateScope()) - { - webUIOptions = scope.ServiceProvider.GetRequiredService>().Value; - webUIOptionsAction?.Invoke(webUIOptions); - } + // Options + var webUIOptions = new WebUIOptions(); + webUIOptionsAction.Invoke(webUIOptions); + var webUIOptionsRegistry = app.ApplicationServices.GetRequiredService(); + webUIOptionsRegistry.Register(webUIOptions); - return app.UseRulesFrameworkWebUI(rulesEngine, webUIOptions); - } + // Blazor + var embeddedProvider = new EmbeddedFileProvider(typeof(WebUIApplicationBuilderExtensions).Assembly, "Rules.Framework.WebUI.Assets"); - /// - /// Uses the rules framework web UI. - /// - /// The application. - /// The rules engine. - /// The web UI options. - /// - private static IApplicationBuilder UseRulesFrameworkWebUI( - this IApplicationBuilder app, - IRulesEngine rulesEngine, - WebUIOptions webUIOptions) - { - var ruleStatusDtoAnalyzer = new RuleStatusDtoAnalyzer(); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = embeddedProvider, + RequestPath = new PathString("/rules-ui") + }); - app.UseMiddleware( - new List + app.UseEndpoints(builder => + { + builder.MapGet("/rules-ui", ctx => { - new GetIndexPageHandler(webUIOptions), - new GetConfigurationsHandler(rulesEngine, webUIOptions), - new GetRulesetsHandler(rulesEngine, ruleStatusDtoAnalyzer, webUIOptions), - new GetRulesHandler(rulesEngine, ruleStatusDtoAnalyzer, webUIOptions) - }, - webUIOptions); + ctx.Response.Redirect("/rules-ui/instance"); + return Task.CompletedTask; + }); + + builder.MapRazorComponents() + .AddInteractiveServerRenderMode(); + }); return app; } diff --git a/src/Rules.Framework.WebUI/WebUIConstants.cs b/src/Rules.Framework.WebUI/WebUIConstants.cs new file mode 100644 index 00000000..cb4e8bd3 --- /dev/null +++ b/src/Rules.Framework.WebUI/WebUIConstants.cs @@ -0,0 +1,39 @@ +namespace Rules.Framework.WebUI +{ + internal static class WebUIConstants + { + public const string IsUniqueInstanceStorageKey = "Is-Unique-Instance"; + + public const string OptionAutoCreateRulesetsDescription = "Behavior of the rules engine when inserting a new rule" + + " whose ruleset does not exist. If set to true, the ruleset will be created before inserting the rule, otherwise rule" + + " insertion will fail."; + + public const string OptionDataTypeDefaultsDescription = "The default of each data type to be used when a specific" + + " condition is not given on 'conditions' parameter when matching (one or all) rules of a particular ruleset."; + + public const string OptionMissingConditionBehaviorDescription = "Behavior of the rules engine when matching (one or all)" + + " rules of a particular ruleset and a specific condition is not given on 'conditions' parameter."; + + public const string OptionMissingConditionBehaviorDiscardDescription = "When a condition is missing, instructs the rules" + + " engine to discard the rule under evaluation (rule is considered not a match)."; + + public const string OptionMissingConditionBehaviorUseDataTypeDefaultDescription = "When a condition is missing, instructs" + + " the rules engine to use the configured data type default for rule's condition."; + + public const string OptionPriorityCriteriaBottommostRuleWinsDescription = "Rules with the highest priority number have greater" + + " priority than the ones with lowest."; + + public const string OptionPriorityCriteriaBottommostRuleWinsName = "Bottommost rule wins"; + + public const string OptionPriorityCriteriaDescription = "Sets the way the rules engine interprets the 'Priority' for each rule."; + + public const string OptionPriorityCriteriaTopmostRuleWinsDescription = "Rules with the lowest priority number have greater" + + " priority than the ones with highest."; + + public const string OptionPriorityCriteriaTopmostRuleWinsName = "Topmost rule wins"; + + public const string SelectedInstanceStorageKey = "Selected-Instance-ID"; + + public const string SelectedRulesetsStorageKey = "Selected-Ruleset-IDs"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/WebUIMiddleware.cs b/src/Rules.Framework.WebUI/WebUIMiddleware.cs deleted file mode 100644 index 9f04643c..00000000 --- a/src/Rules.Framework.WebUI/WebUIMiddleware.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace Rules.Framework.WebUI -{ - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.StaticFiles; - using Microsoft.Extensions.FileProviders; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Options; - -#if NETSTANDARD2_0 - - using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; - -#endif - - internal sealed class WebUIMiddleware - { - private readonly IEnumerable httpRequestHandlers; - private readonly RequestDelegate next; - private readonly StaticFileMiddleware staticFileMiddlewares; - - public WebUIMiddleware( - RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - IEnumerable httpRequestHandlers, - WebUIOptions options) - { - this.httpRequestHandlers = httpRequestHandlers; - this.next = next; - this.staticFileMiddlewares = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options, ".node_modules"); - } - - public async Task InvokeAsync(HttpContext httpContext) - { - var anyHandlerExecuted = await this.ExecuteHandlersAsync(httpContext).ConfigureAwait(false); - if (!anyHandlerExecuted) - { - await this.ExecuteStaticFileMiddlewareAsync(httpContext).ConfigureAwait(true); - } - } - - private static StaticFileMiddleware CreateStaticFileMiddleware(RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - WebUIOptions options, - string path) - { - var asssembly = typeof(WebUIMiddleware).GetTypeInfo().Assembly; - - var provider = new EmbeddedFileProvider(asssembly, asssembly.GetName().Name + path); - - var staticFileOptions = new StaticFileOptions - { - RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", - FileProvider = provider, - ServeUnknownFileTypes = true - }; - - return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); - } - - private async Task ExecuteHandlersAsync(HttpContext httpContext) - { - var results = this.httpRequestHandlers.Select(d => d - .HandleAsync(httpContext.Request, httpContext.Response, this.next)); - - var handle = await Task.WhenAll(results).ConfigureAwait(false); - - if (handle.All(d => !d)) - { - await this.next(httpContext).ConfigureAwait(false); - return false; - } - return true; - } - - private Task ExecuteStaticFileMiddlewareAsync(HttpContext httpContext) - { - return this.staticFileMiddlewares - .Invoke(httpContext); - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/WebUIMvcBuilderExtensions.cs b/src/Rules.Framework.WebUI/WebUIMvcBuilderExtensions.cs new file mode 100644 index 00000000..4ecbede1 --- /dev/null +++ b/src/Rules.Framework.WebUI/WebUIMvcBuilderExtensions.cs @@ -0,0 +1,59 @@ +namespace Rules.Framework.WebUI +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Microsoft.Extensions.DependencyInjection; + using Rules.Framework.WebUI.Services; + + /// + /// Extensions for registering Web UI under the . + /// + [ExcludeFromCodeCoverage] + public static class WebUIMvcBuilderExtensions + { + /// + /// Adds the rules framework web UI to the application, registering the rules engine + /// instances available to the web UI. + /// + /// The MVC core builder. + /// The instances registration action. + /// + public static IMvcCoreBuilder AddRulesFrameworkWebUI(this IMvcCoreBuilder mvcCoreBuilder, Action instancesRegistrationAction) + { + mvcCoreBuilder.Services.AddRulesFrameworkWebUIServices(instancesRegistrationAction); + + return mvcCoreBuilder + .AddApplicationPart(typeof(WebUIMvcBuilderExtensions).Assembly); + } + + /// + /// Adds the rules framework web UI to the application, registering the rules engine + /// instances available to the web UI. + /// + /// The MVC builder. + /// The instances registration action. + /// + public static IMvcBuilder AddRulesFrameworkWebUI(this IMvcBuilder mvcBuilder, Action instancesRegistrationAction) + { + mvcBuilder.Services.AddRulesFrameworkWebUIServices(instancesRegistrationAction); + + return mvcBuilder + .AddApplicationPart(typeof(WebUIMvcBuilderExtensions).Assembly); + } + + private static void AddRulesFrameworkWebUIServices(this IServiceCollection services, Action instancesRegistrationAction) + { + var rulesEngineInstanceProvider = new RulesEngineInstanceProvider(); + instancesRegistrationAction.Invoke(rulesEngineInstanceProvider); + services.AddSingleton(rulesEngineInstanceProvider); + services.AddSingleton(rulesEngineInstanceProvider); + services.AddSingleton(); + services.AddTransient(sp => sp.GetRequiredService().RegisteredOptions); + + // Blazor + services.AddRazorComponents() + .AddInteractiveServerComponents(); + services.AddBlazorBootstrap(); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/WebUIOptions.cs b/src/Rules.Framework.WebUI/WebUIOptions.cs index e5946f31..9361d0ea 100644 --- a/src/Rules.Framework.WebUI/WebUIOptions.cs +++ b/src/Rules.Framework.WebUI/WebUIOptions.cs @@ -1,33 +1,14 @@ namespace Rules.Framework.WebUI { - using System; - using System.IO; - using System.Reflection; - /// /// Options for the Rules Framework Web UI /// public sealed class WebUIOptions { /// - /// Gets title for the Rules Framework Web UI + /// Gets title to present on the Rules Framework Web UI page title. If not specified, will + /// present "Rules Framework" as default. /// - public string DocumentTitle { get; } = "Rules Framework"; - - /// - /// Gets additional content to place in the head of the Rules Framework Web UI - /// - public string HeadContent { get; } = ""; - - /// - /// Gets a Stream function for retrieving the Rules Framework Web UI - /// - public Func IndexStream { get; } = () => typeof(WebUIOptions).GetTypeInfo().Assembly - .GetManifestResourceStream("Rules.Framework.WebUI.index.html"); - - /// - /// Gets or set route prefix for accessing the Rules Framework Web UI - /// - public string RoutePrefix { get; set; } = "rules"; + public string DocumentTitle { get; set; } = "Rules Framework"; } } \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/WebUIOptionsRegistry.cs b/src/Rules.Framework.WebUI/WebUIOptionsRegistry.cs new file mode 100644 index 00000000..2b717784 --- /dev/null +++ b/src/Rules.Framework.WebUI/WebUIOptionsRegistry.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.WebUI +{ + using System; + + internal class WebUIOptionsRegistry + { + public WebUIOptions RegisteredOptions { get; private set; } + + public void Register(WebUIOptions webUIOptions) + { + if (webUIOptions == null) + { + throw new ArgumentNullException(nameof(webUIOptions)); + } + + this.RegisteredOptions = webUIOptions; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/WebUIRequestHandlerBase.cs b/src/Rules.Framework.WebUI/WebUIRequestHandlerBase.cs deleted file mode 100644 index 9ec111c2..00000000 --- a/src/Rules.Framework.WebUI/WebUIRequestHandlerBase.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace Rules.Framework.WebUI -{ - using System; - using System.Linq; - using System.Net; - using System.Text; - using System.Text.Json; - using System.Text.Json.Serialization; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Rules.Framework.WebUI.Dto; - using Rules.Framework.WebUI.Utitlies; - - internal abstract class WebUIRequestHandlerBase : IHttpRequestHandler - { - protected readonly JsonSerializerOptions SerializerOptions; - protected readonly WebUIOptions WebUIOptions; - - protected WebUIRequestHandlerBase(string[] resourcePath, WebUIOptions webUIOptions) - { - this.ResourcePath = resourcePath; - this.WebUIOptions = webUIOptions; - this.SerializerOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - IncludeFields = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - this.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); - this.SerializerOptions.Converters.Add(new PolymorphicWriteOnlyJsonConverter()); - } - - protected abstract HttpMethod HttpMethod { get; } - - protected string[] ResourcePath { get; } - - public virtual async Task HandleAsync(HttpRequest httpRequest, HttpResponse httpResponse, RequestDelegate next) - { - if (!this.CanHandle(httpRequest)) - { - return false; - } - - await this.HandleRequestAsync(httpRequest, httpResponse, next).ConfigureAwait(false); - - return true; - } - - protected bool CanHandle(HttpRequest httpRequest) - { - var resource = httpRequest.Path.ToUriComponent(); - - var resourcesPath = this.ResourcePath.Select(r => string.Format(r, this.WebUIOptions.RoutePrefix)); - - if (!resourcesPath.Contains(resource)) - { - return false; - } - - var method = httpRequest.Method; - - if (!method.Equals(this.HttpMethod.ToString())) - { - return false; - } - - return true; - } - - protected abstract Task HandleRequestAsync(HttpRequest httpRequest, HttpResponse httpResponse, RequestDelegate next); - - protected Task WriteExceptionResponseAsync(HttpResponse httpResponse, Exception exception) - { - var error = new StringBuilder(exception.Message); - if (exception.InnerException != null) - { - error.AppendLine(exception.InnerException.Message); - } - - return this.WriteResponseAsync(httpResponse, error, (int)HttpStatusCode.InternalServerError); - } - - protected virtual async Task WriteResponseAsync(HttpResponse httpResponse, T responseDto, int statusCode) - { - if (!httpResponse.HasStarted) - { - var body = JsonSerializer.Serialize(responseDto, this.SerializerOptions); - httpResponse.StatusCode = statusCode; - httpResponse.ContentType = "application/json"; - httpResponse.Headers.ContentLength = body.Length; - - await httpResponse.WriteAsync(body, Encoding.UTF8).ConfigureAwait(false); - } - } - } -} diff --git a/src/Rules.Framework.WebUI/index.html b/src/Rules.Framework.WebUI/index.html deleted file mode 100644 index 76b8b047..00000000 --- a/src/Rules.Framework.WebUI/index.html +++ /dev/null @@ -1,895 +0,0 @@ - - - - - %(DocumentTitle) - - - - - - %(HeadContent) - - - - - - - -
- - - -
- -
- -
-
-
-

Welcome to Rules.Framework Web UI.
Allows you to see the rules configured in your application.

-
-
- - - - - -
- -
- -
-
-
-
- - - - - - - - - - diff --git a/src/Rules.Framework.WebUI/node_modules/bootstrap/css/bootstrap.min.css b/src/Rules.Framework.WebUI/node_modules/bootstrap/css/bootstrap.min.css deleted file mode 100644 index 1359b3b7..00000000 --- a/src/Rules.Framework.WebUI/node_modules/bootstrap/css/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.2.2 (https://getbootstrap.com/) - * Copyright 2011-2022 The Bootstrap Authors - * Copyright 2011-2022 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid var(--bs-border-color);border-radius:.375rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:calc(1.5em + .75rem + 2px);padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:.375rem}.form-control-color::-webkit-color-swatch{border-radius:.375rem}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + 2px)}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.25rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.5rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;width:100%;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.375rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.375rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.375rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:0.5rem}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:0.25rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:#212529;--bs-dropdown-bg:#fff;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:0.375rem;--bs-dropdown-border-width:1px;--bs-dropdown-inner-border-radius:calc(0.375rem - 1px);--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color:#212529;--bs-dropdown-link-hover-color:#1e2125;--bs-dropdown-link-hover-bg:#e9ecef;--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.375rem}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:1px;--bs-nav-tabs-border-color:#dee2e6;--bs-nav-tabs-border-radius:0.375rem;--bs-nav-tabs-link-hover-border-color:#e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color:#495057;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:0.375rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(0, 0, 0, 0.55);--bs-navbar-hover-color:rgba(0, 0, 0, 0.7);--bs-navbar-disabled-color:rgba(0, 0, 0, 0.3);--bs-navbar-active-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-hover-color:rgba(0, 0, 0, 0.9);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(0, 0, 0, 0.1);--bs-navbar-toggler-border-radius:0.375rem;--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .show>.nav-link{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0, 0, 0, 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:#212529;--bs-accordion-bg:#fff;--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:1px;--bs-accordion-border-radius:0.375rem;--bs-accordion-inner-border-radius:calc(0.375rem - 1px);--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:#212529;--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#0c63e4;--bs-accordion-active-bg:#e7f1ff}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#6c757d;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#6c757d;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:#fff;--bs-pagination-border-width:1px;--bs-pagination-border-color:#dee2e6;--bs-pagination-border-radius:0.375rem;--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:#e9ecef;--bs-pagination-hover-border-color:#dee2e6;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:#e9ecef;--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:#6c757d;--bs-pagination-disabled-bg:#fff;--bs-pagination-disabled-border-color:#dee2e6;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:0.5rem}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:0.25rem}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:0.375rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:1px solid var(--bs-alert-border-color);--bs-alert-border-radius:0.375rem;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:#084298;--bs-alert-bg:#cfe2ff;--bs-alert-border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{--bs-alert-color:#41464b;--bs-alert-bg:#e2e3e5;--bs-alert-border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{--bs-alert-color:#0f5132;--bs-alert-bg:#d1e7dd;--bs-alert-border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{--bs-alert-color:#055160;--bs-alert-bg:#cff4fc;--bs-alert-border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{--bs-alert-color:#664d03;--bs-alert-bg:#fff3cd;--bs-alert-border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{--bs-alert-color:#842029;--bs-alert-bg:#f8d7da;--bs-alert-border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{--bs-alert-color:#636464;--bs-alert-bg:#fefefe;--bs-alert-border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{--bs-alert-color:#141619;--bs-alert-bg:#d3d3d4;--bs-alert-border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#e9ecef;--bs-progress-border-radius:0.375rem;--bs-progress-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:#212529;--bs-list-group-bg:#fff;--bs-list-group-border-color:rgba(0, 0, 0, 0.125);--bs-list-group-border-width:1px;--bs-list-group-border-radius:0.375rem;--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#495057;--bs-list-group-action-hover-color:#495057;--bs-list-group-action-hover-bg:#f8f9fa;--bs-list-group-action-active-color:#212529;--bs-list-group-action-active-bg:#e9ecef;--bs-list-group-disabled-color:#6c757d;--bs-list-group-disabled-bg:#fff;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(255, 255, 255, 0.85);--bs-toast-border-width:1px;--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:0.375rem;--bs-toast-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color:#6c757d;--bs-toast-header-bg:rgba(255, 255, 255, 0.85);--bs-toast-header-border-color:rgba(0, 0, 0, 0.05);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:#fff;--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:1px;--bs-modal-border-radius:0.5rem;--bs-modal-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius:calc(0.5rem - 1px);--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:1px;--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:#fff;--bs-tooltip-bg:#000;--bs-tooltip-border-radius:0.375rem;--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;padding:var(--bs-tooltip-arrow-height);margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#fff;--bs-popover-border-width:1px;--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:0.5rem;--bs-popover-inner-border-radius:calc(0.5rem - 1px);--bs-popover-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color: ;--bs-popover-header-bg:#f0f0f0;--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:#212529;--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color: ;--bs-offcanvas-bg:#fff;--bs-offcanvas-border-width:1px;--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:575.98px){.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}}@media (max-width:575.98px){.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:767.98px){.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}}@media (max-width:767.98px){.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:991.98px){.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}}@media (max-width:991.98px){.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1199.98px){.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}}@media (max-width:1199.98px){.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}}@media (max-width:1399.98px){.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(13,110,253,var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(108,117,125,var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(25,135,84,var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(13,202,240,var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(255,193,7,var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(220,53,69,var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(248,249,250,var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(33,37,41,var(--bs-bg-opacity,1))!important}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-1{--bs-border-width:1px}.border-2{--bs-border-width:2px}.border-3{--bs-border-width:3px}.border-4{--bs-border-width:4px}.border-5{--bs-border-width:5px}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-semibold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/node_modules/bootstrap/dist/bootstrap.bundle.min.js b/src/Rules.Framework.WebUI/node_modules/bootstrap/dist/bootstrap.bundle.min.js deleted file mode 100644 index 1d138863..00000000 --- a/src/Rules.Framework.WebUI/node_modules/bootstrap/dist/bootstrap.bundle.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v5.2.2 (https://getbootstrap.com/) - * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},m=t=>{"function"==typeof t&&t()},_=(e,i,n=!0)=>{if(!n)return void m(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),m(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=N(t);return C.has(o)||(o=t),[n,s,o]}function D(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return j(s,{delegateTarget:r}),n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return j(n,{delegateTarget:t}),i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function S(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function I(t,e,i,n){const s=e[i]||{};for(const o of Object.keys(s))if(o.includes(n)){const n=s[o];S(t,e,i,n.callable,n.delegationSelector)}}function N(t){return t=t.replace(y,""),T[t]||t}const P={on(t,e,i,n){D(t,e,i,n,!1)},one(t,e,i,n){D(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))I(t,l,i,e.slice(1));for(const i of Object.keys(c)){const n=i.replace(w,"");if(!a||e.includes(n)){const e=c[i];S(t,l,r,e.callable,e.delegationSelector)}}}else{if(!Object.keys(c).length)return;S(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==N(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());let l=new Event(e,{bubbles:o,cancelable:!0});return l=j(l,i),a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function j(t,e){for(const[i,n]of Object.entries(e||{}))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}const M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};function $(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function W(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const B={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${W(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${W(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=$(t.dataset[n])}return e},getDataAttribute:(t,e)=>$(t.getAttribute(`data-bs-${W(e)}`))};class F{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?B.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?B.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const n of Object.keys(e)){const s=e[n],r=t[n],a=o(r)?"element":null==(i=r)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(a))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}var i}}class z extends F{constructor(t,e){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(e),H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.2.2"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const q=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;P.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class R extends z{static get NAME(){return"alert"}close(){if(P.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),P.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=R.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}q(R,"close"),g(R);const V='[data-bs-toggle="button"]';class K extends z{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=K.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}P.on(document,"click.bs.button.data-api",V,(t=>{t.preventDefault();const e=t.target.closest(V);K.getOrCreateInstance(e).toggle()})),g(K);const Q={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))}},X={endCallback:null,leftCallback:null,rightCallback:null},Y={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class U extends F{constructor(t,e){super(),this._element=t,t&&U.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return X}static get DefaultType(){return Y}static get NAME(){return"swipe"}dispose(){P.off(this._element,".bs.swipe")}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),m(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&m(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(P.on(this._element,"pointerdown.bs.swipe",(t=>this._start(t))),P.on(this._element,"pointerup.bs.swipe",(t=>this._end(t))),this._element.classList.add("pointer-event")):(P.on(this._element,"touchstart.bs.swipe",(t=>this._start(t))),P.on(this._element,"touchmove.bs.swipe",(t=>this._move(t))),P.on(this._element,"touchend.bs.swipe",(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const G="next",J="prev",Z="left",tt="right",et="slid.bs.carousel",it="carousel",nt="active",st={ArrowLeft:tt,ArrowRight:Z},ot={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},rt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class at extends z{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=Q.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===it&&this.cycle()}static get Default(){return ot}static get DefaultType(){return rt}static get NAME(){return"carousel"}next(){this._slide(G)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(J)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?P.one(this._element,et,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void P.one(this._element,et,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?G:J;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&P.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(P.on(this._element,"mouseenter.bs.carousel",(()=>this.pause())),P.on(this._element,"mouseleave.bs.carousel",(()=>this._maybeEnableCycle()))),this._config.touch&&U.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of Q.find(".carousel-item img",this._element))P.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(Z)),rightCallback:()=>this._slide(this._directionToOrder(tt)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new U(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=st[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=Q.findOne(".active",this._indicatorsElement);e.classList.remove(nt),e.removeAttribute("aria-current");const i=Q.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(nt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===G,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>P.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r("slide.bs.carousel").defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(nt),i.classList.remove(nt,c,l),this._isSliding=!1,r(et)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return Q.findOne(".active.carousel-item",this._element)}_getItems(){return Q.find(".carousel-item",this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===Z?J:G:t===Z?G:J}_orderToDirection(t){return p()?t===J?Z:tt:t===J?tt:Z}static jQueryInterface(t){return this.each((function(){const e=at.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}P.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",(function(t){const e=n(this);if(!e||!e.classList.contains(it))return;t.preventDefault();const i=at.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===B.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),P.on(window,"load.bs.carousel.data-api",(()=>{const t=Q.find('[data-bs-ride="carousel"]');for(const e of t)at.getOrCreateInstance(e)})),g(at);const lt="show",ct="collapse",ht="collapsing",dt='[data-bs-toggle="collapse"]',ut={parent:null,toggle:!0},ft={parent:"(null|element)",toggle:"boolean"};class pt extends z{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const n=Q.find(dt);for(const t of n){const e=i(t),n=Q.find(e).filter((t=>t===this._element));null!==e&&n.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return ut}static get DefaultType(){return ft}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>pt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(P.trigger(this._element,"show.bs.collapse").defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[e]="",P.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(P.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);for(const t of this._triggerArray){const e=n(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),P.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(dt);for(const e of t){const t=n(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=Q.find(":scope .collapse .collapse",this._config.parent);return Q.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}P.on(document,"click.bs.collapse.data-api",dt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this),n=Q.find(e);for(const t of n)pt.getOrCreateInstance(t,{toggle:!1}).toggle()})),g(pt);var gt="top",mt="bottom",_t="right",bt="left",vt="auto",yt=[gt,mt,_t,bt],wt="start",At="end",Et="clippingParents",Tt="viewport",Ct="popper",Ot="reference",xt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+At])}),[]),kt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+At])}),[]),Lt="beforeRead",Dt="read",St="afterRead",It="beforeMain",Nt="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",$t=[Lt,Dt,St,It,Nt,Pt,jt,Mt,Ht];function Wt(t){return t?(t.nodeName||"").toLowerCase():null}function Bt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function Ft(t){return t instanceof Bt(t).Element||t instanceof Element}function zt(t){return t instanceof Bt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Bt(t).ShadowRoot||t instanceof ShadowRoot)}const Rt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Wt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Wt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Vt(t){return t.split("-")[0]}var Kt=Math.max,Qt=Math.min,Xt=Math.round;function Yt(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ut(){return!/^((?!chrome|android).)*safari/i.test(Yt())}function Gt(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&zt(t)&&(s=t.offsetWidth>0&&Xt(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Xt(n.height)/t.offsetHeight||1);var r=(Ft(t)?Bt(t):window).visualViewport,a=!Ut()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Jt(t){var e=Gt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Zt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function te(t){return Bt(t).getComputedStyle(t)}function ee(t){return["table","td","th"].indexOf(Wt(t))>=0}function ie(t){return((Ft(t)?t.ownerDocument:t.document)||window.document).documentElement}function ne(t){return"html"===Wt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||ie(t)}function se(t){return zt(t)&&"fixed"!==te(t).position?t.offsetParent:null}function oe(t){for(var e=Bt(t),i=se(t);i&&ee(i)&&"static"===te(i).position;)i=se(i);return i&&("html"===Wt(i)||"body"===Wt(i)&&"static"===te(i).position)?e:i||function(t){var e=/firefox/i.test(Yt());if(/Trident/i.test(Yt())&&zt(t)&&"fixed"===te(t).position)return null;var i=ne(t);for(qt(i)&&(i=i.host);zt(i)&&["html","body"].indexOf(Wt(i))<0;){var n=te(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function re(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function ae(t,e,i){return Kt(t,Qt(e,i))}function le(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ce(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const he={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Vt(i.placement),l=re(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return le("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ce(t,yt))}(s.padding,i),d=Jt(o),u="y"===l?gt:bt,f="y"===l?mt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],g=r[l]-i.rects.reference[l],m=oe(o),_=m?"y"===l?m.clientHeight||0:m.clientWidth||0:0,b=p/2-g/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=ae(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Zt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function de(t){return t.split("-")[1]}var ue={top:"auto",right:"auto",bottom:"auto",left:"auto"};function fe(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,g=void 0===p?0:p,m="function"==typeof h?h({x:f,y:g}):{x:f,y:g};f=m.x,g=m.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=bt,y=gt,w=window;if(c){var A=oe(i),E="clientHeight",T="clientWidth";A===Bt(i)&&"static"!==te(A=ie(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===gt||(s===bt||s===_t)&&o===At)&&(y=mt,g-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,g*=l?1:-1),s!==bt&&(s!==gt&&s!==mt||o!==At)||(v=_t,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&ue),x=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:Xt(e*n)/n||0,y:Xt(i*n)/n||0}}({x:f,y:g}):{x:f,y:g};return f=x.x,g=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+g+"px)":"translate3d("+f+"px, "+g+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?g+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const pe={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Vt(e.placement),variation:de(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,fe(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,fe(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ge={passive:!0};const me={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Bt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ge)})),a&&l.addEventListener("resize",i.update,ge),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ge)})),a&&l.removeEventListener("resize",i.update,ge)}},data:{}};var _e={left:"right",right:"left",bottom:"top",top:"bottom"};function be(t){return t.replace(/left|right|bottom|top/g,(function(t){return _e[t]}))}var ve={start:"end",end:"start"};function ye(t){return t.replace(/start|end/g,(function(t){return ve[t]}))}function we(t){var e=Bt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ae(t){return Gt(ie(t)).left+we(t).scrollLeft}function Ee(t){var e=te(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Te(t){return["html","body","#document"].indexOf(Wt(t))>=0?t.ownerDocument.body:zt(t)&&Ee(t)?t:Te(ne(t))}function Ce(t,e){var i;void 0===e&&(e=[]);var n=Te(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Bt(n),r=s?[o].concat(o.visualViewport||[],Ee(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ce(ne(r)))}function Oe(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function xe(t,e,i){return e===Tt?Oe(function(t,e){var i=Bt(t),n=ie(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ut();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ae(t),y:l}}(t,i)):Ft(e)?function(t,e){var i=Gt(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Oe(function(t){var e,i=ie(t),n=we(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=Kt(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=Kt(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ae(t),l=-n.scrollTop;return"rtl"===te(s||i).direction&&(a+=Kt(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(ie(t)))}function ke(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Vt(s):null,r=s?de(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case gt:e={x:a,y:i.y-n.height};break;case mt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?re(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case At:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Et:a,c=i.rootBoundary,h=void 0===c?Tt:c,d=i.elementContext,u=void 0===d?Ct:d,f=i.altBoundary,p=void 0!==f&&f,g=i.padding,m=void 0===g?0:g,_=le("number"!=typeof m?m:ce(m,yt)),b=u===Ct?Ot:Ct,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Ce(ne(t)),i=["absolute","fixed"].indexOf(te(t).position)>=0&&zt(t)?oe(t):t;return Ft(i)?e.filter((function(t){return Ft(t)&&Zt(t,i)&&"body"!==Wt(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=xe(t,i,n);return e.top=Kt(s.top,e.top),e.right=Qt(s.right,e.right),e.bottom=Qt(s.bottom,e.bottom),e.left=Kt(s.left,e.left),e}),xe(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(Ft(y)?y:y.contextElement||ie(t.elements.popper),l,h,r),A=Gt(t.elements.reference),E=ke({reference:A,element:v,strategy:"absolute",placement:s}),T=Oe(Object.assign({},v,E)),C=u===Ct?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Ct&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[_t,mt].indexOf(t)>=0?1:-1,i=[gt,mt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function De(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?kt:l,h=de(n),d=h?a?xt:xt.filter((function(t){return de(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=Le(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Vt(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const Se={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,g=i.allowedAutoPlacements,m=e.options.placement,_=Vt(m),b=l||(_!==m&&p?function(t){if(Vt(t)===vt)return[];var e=be(t);return[ye(t),e,ye(e)]}(m):[be(m)]),v=[m].concat(b).reduce((function(t,i){return t.concat(Vt(i)===vt?De(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:g}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,D=L?"width":"height",S=Le(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),I=L?k?_t:bt:k?mt:gt;y[D]>w[D]&&(I=be(I));var N=be(I),P=[];if(o&&P.push(S[x]<=0),a&&P.push(S[I]<=0,S[N]<=0),P.every((function(t){return t}))){T=O,E=!1;break}A.set(O,P)}if(E)for(var j=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Ie(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Ne(t){return[gt,_t,mt,bt].some((function(e){return t[e]>=0}))}const Pe={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=Le(e,{elementContext:"reference"}),a=Le(e,{altBoundary:!0}),l=Ie(r,n),c=Ie(a,s,o),h=Ne(l),d=Ne(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},je={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=kt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Vt(t),s=[bt,gt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Me={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ke({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},He={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,g=void 0===p?0:p,m=Le(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Vt(e.placement),b=de(e.placement),v=!b,y=re(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof g?g(Object.assign({},e.rects,{placement:e.placement})):g,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,D="y"===y?gt:bt,S="y"===y?mt:_t,I="y"===y?"height":"width",N=A[y],P=N+m[D],j=N-m[S],M=f?-T[I]/2:0,H=b===wt?E[I]:T[I],$=b===wt?-T[I]:-E[I],W=e.elements.arrow,B=f&&W?Jt(W):{width:0,height:0},F=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=F[D],q=F[S],R=ae(0,E[I],B[I]),V=v?E[I]/2-M-R-z-O.mainAxis:H-R-z-O.mainAxis,K=v?-E[I]/2+M+R+q+O.mainAxis:$+R+q+O.mainAxis,Q=e.elements.arrow&&oe(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=N+K-Y,G=ae(f?Qt(P,N+V-Y-X):P,N,f?Kt(j,U):j);A[y]=G,k[y]=G-N}if(a){var J,Z="x"===y?gt:bt,tt="x"===y?mt:_t,et=A[w],it="y"===w?"height":"width",nt=et+m[Z],st=et-m[tt],ot=-1!==[gt,bt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=ae(t,e,i);return n>i?i:n}(at,et,lt):ae(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function $e(t,e,i){void 0===i&&(i=!1);var n,s,o=zt(e),r=zt(e)&&function(t){var e=t.getBoundingClientRect(),i=Xt(e.width)/t.offsetWidth||1,n=Xt(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=ie(e),l=Gt(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==Wt(e)||Ee(a))&&(c=(n=e)!==Bt(n)&&zt(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:we(n)),zt(e)?((h=Gt(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ae(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function We(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Fe(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(B.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=Q.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=Q.find(ti);for(const i of e){const e=hi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Xe,Ye].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ze)?this:Q.prev(this,Ze)[0]||Q.next(this,Ze)[0]||Q.findOne(Ze,t.delegateTarget.parentNode),o=hi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}P.on(document,Ge,Ze,hi.dataApiKeydownHandler),P.on(document,Ge,ei,hi.dataApiKeydownHandler),P.on(document,Ue,hi.clearMenus),P.on(document,"keyup.bs.dropdown.data-api",hi.clearMenus),P.on(document,Ue,Ze,(function(t){t.preventDefault(),hi.getOrCreateInstance(this).toggle()})),g(hi);const di=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",ui=".sticky-top",fi="padding-right",pi="margin-right";class gi{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,fi,(e=>e+t)),this._setElementAttributes(di,fi,(e=>e+t)),this._setElementAttributes(ui,pi,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,fi),this._resetElementAttributes(di,fi),this._resetElementAttributes(ui,pi)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&B.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=B.getDataAttribute(t,e);null!==i?(B.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of Q.find(t,this._element))e(i)}}const mi="show",_i="mousedown.bs.backdrop",bi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},vi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class yi extends F{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return bi}static get DefaultType(){return vi}static get NAME(){return"backdrop"}show(t){if(!this._config.isVisible)return void m(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(mi),this._emulateAnimation((()=>{m(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(mi),this._emulateAnimation((()=>{this.dispose(),m(t)}))):m(t)}dispose(){this._isAppended&&(P.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),P.on(t,_i,(()=>{m(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const wi=".bs.focustrap",Ai="backward",Ei={autofocus:!0,trapElement:null},Ti={autofocus:"boolean",trapElement:"element"};class Ci extends F{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Ei}static get DefaultType(){return Ti}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),P.off(document,wi),P.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),P.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,P.off(document,wi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=Q.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===Ai?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ai:"forward")}}const Oi="hidden.bs.modal",xi="show.bs.modal",ki="modal-open",Li="show",Di="modal-static",Si={backdrop:!0,focus:!0,keyboard:!0},Ii={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ni extends z{constructor(t,e){super(t,e),this._dialog=Q.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new gi,this._addEventListeners()}static get Default(){return Si}static get DefaultType(){return Ii}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(ki),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(P.trigger(this._element,"hide.bs.modal").defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Li),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){for(const t of[window,this._dialog])P.off(t,".bs.modal");this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new yi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ci({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=Q.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(Li),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,P.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.modal",(t=>{if("Escape"===t.key)return this._config.keyboard?(t.preventDefault(),void this.hide()):void this._triggerBackdropTransition()})),P.on(window,"resize.bs.modal",(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),P.on(this._element,"mousedown.dismiss.bs.modal",(t=>{P.one(this._element,"click.dismiss.bs.modal",(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(ki),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,Oi)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Di)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Di),this._queueCallback((()=>{this._element.classList.remove(Di),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ni.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}P.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,xi,(t=>{t.defaultPrevented||P.one(e,Oi,(()=>{a(this)&&this.focus()}))}));const i=Q.findOne(".modal.show");i&&Ni.getInstance(i).hide(),Ni.getOrCreateInstance(e).toggle(this)})),q(Ni),g(Ni);const Pi="show",ji="showing",Mi="hiding",Hi=".offcanvas.show",$i="hidePrevented.bs.offcanvas",Wi="hidden.bs.offcanvas",Bi={backdrop:!0,keyboard:!0,scroll:!1},Fi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class zi extends z{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Bi}static get DefaultType(){return Fi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new gi).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(ji),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Pi),this._element.classList.remove(ji),P.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Mi),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Pi,Mi),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new gi).reset(),P.trigger(this._element,Wi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new yi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():P.trigger(this._element,$i)}:null})}_initializeFocusTrap(){return new Ci({trapElement:this._element})}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():P.trigger(this._element,$i))}))}static jQueryInterface(t){return this.each((function(){const e=zi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}P.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;P.one(e,Wi,(()=>{a(this)&&this.focus()}));const i=Q.findOne(Hi);i&&i!==e&&zi.getInstance(i).hide(),zi.getOrCreateInstance(e).toggle(this)})),P.on(window,"load.bs.offcanvas.data-api",(()=>{for(const t of Q.find(Hi))zi.getOrCreateInstance(t).show()})),P.on(window,"resize.bs.offcanvas",(()=>{for(const t of Q.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&zi.getOrCreateInstance(t).hide()})),q(zi),g(zi);const qi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Ri=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Vi=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ki=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!qi.has(i)||Boolean(Ri.test(t.nodeValue)||Vi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Qi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xi={allowList:Qi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Yi={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ui={entry:"(string|element|function|null)",selector:"(string|element)"};class Gi extends F{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ui)}_setContent(t,e,i){const n=Q.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Ki(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return"function"==typeof t?t(this):t}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Ji=new Set(["sanitize","allowList","sanitizeFn"]),Zi="fade",tn="show",en=".modal",nn="hide.bs.modal",sn="hover",on="focus",rn={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},an={allowList:Qi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ln={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cn extends z{constructor(t,e){if(void 0===Ke)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return an}static get DefaultType(){return ln}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(en),nn,this._hideModalHandler),this.tip&&this.tip.remove(),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this.tip&&(this.tip.remove(),this.tip=null);const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),P.trigger(this._element,this.constructor.eventName("inserted"))),this._popper?this._popper.update():this._popper=this._createPopper(i),i.classList.add(tn),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.on(t,"mouseover",h);this._queueCallback((()=>{P.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(!this._isShown())return;if(P.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented)return;const t=this._getTipElement();if(t.classList.remove(tn),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||t.remove(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.eventName("hidden")),this._disposePopper())}),this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(Zi,tn),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(Zi),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Gi({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Zi)}_isShown(){return this.tip&&this.tip.classList.contains(tn)}_createPopper(t){const e="function"==typeof this._config.placement?this._config.placement.call(this,t,this._element):this._config.placement,i=rn[e.toUpperCase()];return Ve(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)P.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===sn?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===sn?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");P.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?on:sn]=!0,e._enter()})),P.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?on:sn]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(en),nn,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=B.getDataAttributes(this._element);for(const t of Object.keys(e))Ji.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=cn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(cn);const hn={...cn.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},dn={...cn.DefaultType,content:"(null|string|element|function)"};class un extends cn{static get Default(){return hn}static get DefaultType(){return dn}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn="click.bs.scrollspy",pn="active",gn="[href]",mn={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},_n={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class bn extends z{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return mn}static get DefaultType(){return _n}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(P.off(this._config.target,fn),P.on(this._config.target,fn,gn,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=Q.find(gn,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=Q.findOne(e.hash,this._element);a(t)&&(this._targetLinks.set(e.hash,e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(pn),this._activateParents(t),P.trigger(this._element,"activate.bs.scrollspy",{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))Q.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(pn);else for(const e of Q.parents(t,".nav, .list-group"))for(const t of Q.prev(e,".nav-link, .nav-item > .nav-link, .list-group-item"))t.classList.add(pn)}_clearActiveClass(t){t.classList.remove(pn);const e=Q.find("[href].active",t);for(const t of e)t.classList.remove(pn)}static jQueryInterface(t){return this.each((function(){const e=bn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(window,"load.bs.scrollspy.data-api",(()=>{for(const t of Q.find('[data-bs-spy="scroll"]'))bn.getOrCreateInstance(t)})),g(bn);const vn="ArrowLeft",yn="ArrowRight",wn="ArrowUp",An="ArrowDown",En="active",Tn="fade",Cn="show",On='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',xn=`.nav-link:not(.dropdown-toggle), .list-group-item:not(.dropdown-toggle), [role="tab"]:not(.dropdown-toggle), ${On}`;class kn extends z{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),P.on(this._element,"keydown.bs.tab",(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?P.trigger(e,"hide.bs.tab",{relatedTarget:t}):null;P.trigger(t,"show.bs.tab",{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(En),this._activate(n(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),P.trigger(t,"shown.bs.tab",{relatedTarget:e})):t.classList.add(Cn)}),t,t.classList.contains(Tn)))}_deactivate(t,e){t&&(t.classList.remove(En),t.blur(),this._deactivate(n(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),P.trigger(t,"hidden.bs.tab",{relatedTarget:e})):t.classList.remove(Cn)}),t,t.classList.contains(Tn)))}_keydown(t){if(![vn,yn,wn,An].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=[yn,An].includes(t.key),i=b(this._getChildren().filter((t=>!l(t))),t.target,e,!0);i&&(i.focus({preventScroll:!0}),kn.getOrCreateInstance(i).show())}_getChildren(){return Q.find(xn,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=n(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`#${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=Q.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",En),n(".dropdown-menu",Cn),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(En)}_getInnerElement(t){return t.matches(xn)?t:Q.findOne(xn,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=kn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(document,"click.bs.tab",On,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||kn.getOrCreateInstance(this).show()})),P.on(window,"load.bs.tab",(()=>{for(const t of Q.find('.active[data-bs-toggle="tab"], .active[data-bs-toggle="pill"], .active[data-bs-toggle="list"]'))kn.getOrCreateInstance(t)})),g(kn);const Ln="hide",Dn="show",Sn="showing",In={animation:"boolean",autohide:"boolean",delay:"number"},Nn={animation:!0,autohide:!0,delay:5e3};class Pn extends z{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Nn}static get DefaultType(){return In}static get NAME(){return"toast"}show(){P.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Ln),d(this._element),this._element.classList.add(Dn,Sn),this._queueCallback((()=>{this._element.classList.remove(Sn),P.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(P.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(Sn),this._queueCallback((()=>{this._element.classList.add(Ln),this._element.classList.remove(Sn,Dn),P.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Dn),super.dispose()}isShown(){return this._element.classList.contains(Dn)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),P.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Pn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return q(Pn),g(Pn),{Alert:R,Button:K,Carousel:at,Collapse:pt,Dropdown:hi,Modal:Ni,Offcanvas:zi,Popover:un,ScrollSpy:bn,Tab:kn,Toast:Pn,Tooltip:cn}})); -//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.css b/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.css deleted file mode 100644 index 133e93ed..00000000 --- a/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.css +++ /dev/null @@ -1,819 +0,0 @@ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -/*! - * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=76290a4e23bf2c3f61a4e17625be4627) - * Config saved to config.json and https://gist.github.com/76290a4e23bf2c3f61a4e17625be4627 - */ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk:before { - content: "\002a"; -} -.glyphicon-plus:before { - content: "\002b"; -} -.glyphicon-euro:before, -.glyphicon-eur:before { - content: "\20ac"; -} -.glyphicon-minus:before { - content: "\2212"; -} -.glyphicon-cloud:before { - content: "\2601"; -} -.glyphicon-envelope:before { - content: "\2709"; -} -.glyphicon-pencil:before { - content: "\270f"; -} -.glyphicon-glass:before { - content: "\e001"; -} -.glyphicon-music:before { - content: "\e002"; -} -.glyphicon-search:before { - content: "\e003"; -} -.glyphicon-heart:before { - content: "\e005"; -} -.glyphicon-star:before { - content: "\e006"; -} -.glyphicon-star-empty:before { - content: "\e007"; -} -.glyphicon-user:before { - content: "\e008"; -} -.glyphicon-film:before { - content: "\e009"; -} -.glyphicon-th-large:before { - content: "\e010"; -} -.glyphicon-th:before { - content: "\e011"; -} -.glyphicon-th-list:before { - content: "\e012"; -} -.glyphicon-ok:before { - content: "\e013"; -} -.glyphicon-remove:before { - content: "\e014"; -} -.glyphicon-zoom-in:before { - content: "\e015"; -} -.glyphicon-zoom-out:before { - content: "\e016"; -} -.glyphicon-off:before { - content: "\e017"; -} -.glyphicon-signal:before { - content: "\e018"; -} -.glyphicon-cog:before { - content: "\e019"; -} -.glyphicon-trash:before { - content: "\e020"; -} -.glyphicon-home:before { - content: "\e021"; -} -.glyphicon-file:before { - content: "\e022"; -} -.glyphicon-time:before { - content: "\e023"; -} -.glyphicon-road:before { - content: "\e024"; -} -.glyphicon-download-alt:before { - content: "\e025"; -} -.glyphicon-download:before { - content: "\e026"; -} -.glyphicon-upload:before { - content: "\e027"; -} -.glyphicon-inbox:before { - content: "\e028"; -} -.glyphicon-play-circle:before { - content: "\e029"; -} -.glyphicon-repeat:before { - content: "\e030"; -} -.glyphicon-refresh:before { - content: "\e031"; -} -.glyphicon-list-alt:before { - content: "\e032"; -} -.glyphicon-lock:before { - content: "\e033"; -} -.glyphicon-flag:before { - content: "\e034"; -} -.glyphicon-headphones:before { - content: "\e035"; -} -.glyphicon-volume-off:before { - content: "\e036"; -} -.glyphicon-volume-down:before { - content: "\e037"; -} -.glyphicon-volume-up:before { - content: "\e038"; -} -.glyphicon-qrcode:before { - content: "\e039"; -} -.glyphicon-barcode:before { - content: "\e040"; -} -.glyphicon-tag:before { - content: "\e041"; -} -.glyphicon-tags:before { - content: "\e042"; -} -.glyphicon-book:before { - content: "\e043"; -} -.glyphicon-bookmark:before { - content: "\e044"; -} -.glyphicon-print:before { - content: "\e045"; -} -.glyphicon-camera:before { - content: "\e046"; -} -.glyphicon-font:before { - content: "\e047"; -} -.glyphicon-bold:before { - content: "\e048"; -} -.glyphicon-italic:before { - content: "\e049"; -} -.glyphicon-text-height:before { - content: "\e050"; -} -.glyphicon-text-width:before { - content: "\e051"; -} -.glyphicon-align-left:before { - content: "\e052"; -} -.glyphicon-align-center:before { - content: "\e053"; -} -.glyphicon-align-right:before { - content: "\e054"; -} -.glyphicon-align-justify:before { - content: "\e055"; -} -.glyphicon-list:before { - content: "\e056"; -} -.glyphicon-indent-left:before { - content: "\e057"; -} -.glyphicon-indent-right:before { - content: "\e058"; -} -.glyphicon-facetime-video:before { - content: "\e059"; -} -.glyphicon-picture:before { - content: "\e060"; -} -.glyphicon-map-marker:before { - content: "\e062"; -} -.glyphicon-adjust:before { - content: "\e063"; -} -.glyphicon-tint:before { - content: "\e064"; -} -.glyphicon-edit:before { - content: "\e065"; -} -.glyphicon-share:before { - content: "\e066"; -} -.glyphicon-check:before { - content: "\e067"; -} -.glyphicon-move:before { - content: "\e068"; -} -.glyphicon-step-backward:before { - content: "\e069"; -} -.glyphicon-fast-backward:before { - content: "\e070"; -} -.glyphicon-backward:before { - content: "\e071"; -} -.glyphicon-play:before { - content: "\e072"; -} -.glyphicon-pause:before { - content: "\e073"; -} -.glyphicon-stop:before { - content: "\e074"; -} -.glyphicon-forward:before { - content: "\e075"; -} -.glyphicon-fast-forward:before { - content: "\e076"; -} -.glyphicon-step-forward:before { - content: "\e077"; -} -.glyphicon-eject:before { - content: "\e078"; -} -.glyphicon-chevron-left:before { - content: "\e079"; -} -.glyphicon-chevron-right:before { - content: "\e080"; -} -.glyphicon-plus-sign:before { - content: "\e081"; -} -.glyphicon-minus-sign:before { - content: "\e082"; -} -.glyphicon-remove-sign:before { - content: "\e083"; -} -.glyphicon-ok-sign:before { - content: "\e084"; -} -.glyphicon-question-sign:before { - content: "\e085"; -} -.glyphicon-info-sign:before { - content: "\e086"; -} -.glyphicon-screenshot:before { - content: "\e087"; -} -.glyphicon-remove-circle:before { - content: "\e088"; -} -.glyphicon-ok-circle:before { - content: "\e089"; -} -.glyphicon-ban-circle:before { - content: "\e090"; -} -.glyphicon-arrow-left:before { - content: "\e091"; -} -.glyphicon-arrow-right:before { - content: "\e092"; -} -.glyphicon-arrow-up:before { - content: "\e093"; -} -.glyphicon-arrow-down:before { - content: "\e094"; -} -.glyphicon-share-alt:before { - content: "\e095"; -} -.glyphicon-resize-full:before { - content: "\e096"; -} -.glyphicon-resize-small:before { - content: "\e097"; -} -.glyphicon-exclamation-sign:before { - content: "\e101"; -} -.glyphicon-gift:before { - content: "\e102"; -} -.glyphicon-leaf:before { - content: "\e103"; -} -.glyphicon-fire:before { - content: "\e104"; -} -.glyphicon-eye-open:before { - content: "\e105"; -} -.glyphicon-eye-close:before { - content: "\e106"; -} -.glyphicon-warning-sign:before { - content: "\e107"; -} -.glyphicon-plane:before { - content: "\e108"; -} -.glyphicon-calendar:before { - content: "\e109"; -} -.glyphicon-random:before { - content: "\e110"; -} -.glyphicon-comment:before { - content: "\e111"; -} -.glyphicon-magnet:before { - content: "\e112"; -} -.glyphicon-chevron-up:before { - content: "\e113"; -} -.glyphicon-chevron-down:before { - content: "\e114"; -} -.glyphicon-retweet:before { - content: "\e115"; -} -.glyphicon-shopping-cart:before { - content: "\e116"; -} -.glyphicon-folder-close:before { - content: "\e117"; -} -.glyphicon-folder-open:before { - content: "\e118"; -} -.glyphicon-resize-vertical:before { - content: "\e119"; -} -.glyphicon-resize-horizontal:before { - content: "\e120"; -} -.glyphicon-hdd:before { - content: "\e121"; -} -.glyphicon-bullhorn:before { - content: "\e122"; -} -.glyphicon-bell:before { - content: "\e123"; -} -.glyphicon-certificate:before { - content: "\e124"; -} -.glyphicon-thumbs-up:before { - content: "\e125"; -} -.glyphicon-thumbs-down:before { - content: "\e126"; -} -.glyphicon-hand-right:before { - content: "\e127"; -} -.glyphicon-hand-left:before { - content: "\e128"; -} -.glyphicon-hand-up:before { - content: "\e129"; -} -.glyphicon-hand-down:before { - content: "\e130"; -} -.glyphicon-circle-arrow-right:before { - content: "\e131"; -} -.glyphicon-circle-arrow-left:before { - content: "\e132"; -} -.glyphicon-circle-arrow-up:before { - content: "\e133"; -} -.glyphicon-circle-arrow-down:before { - content: "\e134"; -} -.glyphicon-globe:before { - content: "\e135"; -} -.glyphicon-wrench:before { - content: "\e136"; -} -.glyphicon-tasks:before { - content: "\e137"; -} -.glyphicon-filter:before { - content: "\e138"; -} -.glyphicon-briefcase:before { - content: "\e139"; -} -.glyphicon-fullscreen:before { - content: "\e140"; -} -.glyphicon-dashboard:before { - content: "\e141"; -} -.glyphicon-paperclip:before { - content: "\e142"; -} -.glyphicon-heart-empty:before { - content: "\e143"; -} -.glyphicon-link:before { - content: "\e144"; -} -.glyphicon-phone:before { - content: "\e145"; -} -.glyphicon-pushpin:before { - content: "\e146"; -} -.glyphicon-usd:before { - content: "\e148"; -} -.glyphicon-gbp:before { - content: "\e149"; -} -.glyphicon-sort:before { - content: "\e150"; -} -.glyphicon-sort-by-alphabet:before { - content: "\e151"; -} -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; -} -.glyphicon-sort-by-order:before { - content: "\e153"; -} -.glyphicon-sort-by-order-alt:before { - content: "\e154"; -} -.glyphicon-sort-by-attributes:before { - content: "\e155"; -} -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; -} -.glyphicon-unchecked:before { - content: "\e157"; -} -.glyphicon-expand:before { - content: "\e158"; -} -.glyphicon-collapse-down:before { - content: "\e159"; -} -.glyphicon-collapse-up:before { - content: "\e160"; -} -.glyphicon-log-in:before { - content: "\e161"; -} -.glyphicon-flash:before { - content: "\e162"; -} -.glyphicon-log-out:before { - content: "\e163"; -} -.glyphicon-new-window:before { - content: "\e164"; -} -.glyphicon-record:before { - content: "\e165"; -} -.glyphicon-save:before { - content: "\e166"; -} -.glyphicon-open:before { - content: "\e167"; -} -.glyphicon-saved:before { - content: "\e168"; -} -.glyphicon-import:before { - content: "\e169"; -} -.glyphicon-export:before { - content: "\e170"; -} -.glyphicon-send:before { - content: "\e171"; -} -.glyphicon-floppy-disk:before { - content: "\e172"; -} -.glyphicon-floppy-saved:before { - content: "\e173"; -} -.glyphicon-floppy-remove:before { - content: "\e174"; -} -.glyphicon-floppy-save:before { - content: "\e175"; -} -.glyphicon-floppy-open:before { - content: "\e176"; -} -.glyphicon-credit-card:before { - content: "\e177"; -} -.glyphicon-transfer:before { - content: "\e178"; -} -.glyphicon-cutlery:before { - content: "\e179"; -} -.glyphicon-header:before { - content: "\e180"; -} -.glyphicon-compressed:before { - content: "\e181"; -} -.glyphicon-earphone:before { - content: "\e182"; -} -.glyphicon-phone-alt:before { - content: "\e183"; -} -.glyphicon-tower:before { - content: "\e184"; -} -.glyphicon-stats:before { - content: "\e185"; -} -.glyphicon-sd-video:before { - content: "\e186"; -} -.glyphicon-hd-video:before { - content: "\e187"; -} -.glyphicon-subtitles:before { - content: "\e188"; -} -.glyphicon-sound-stereo:before { - content: "\e189"; -} -.glyphicon-sound-dolby:before { - content: "\e190"; -} -.glyphicon-sound-5-1:before { - content: "\e191"; -} -.glyphicon-sound-6-1:before { - content: "\e192"; -} -.glyphicon-sound-7-1:before { - content: "\e193"; -} -.glyphicon-copyright-mark:before { - content: "\e194"; -} -.glyphicon-registration-mark:before { - content: "\e195"; -} -.glyphicon-cloud-download:before { - content: "\e197"; -} -.glyphicon-cloud-upload:before { - content: "\e198"; -} -.glyphicon-tree-conifer:before { - content: "\e199"; -} -.glyphicon-tree-deciduous:before { - content: "\e200"; -} -.glyphicon-cd:before { - content: "\e201"; -} -.glyphicon-save-file:before { - content: "\e202"; -} -.glyphicon-open-file:before { - content: "\e203"; -} -.glyphicon-level-up:before { - content: "\e204"; -} -.glyphicon-copy:before { - content: "\e205"; -} -.glyphicon-paste:before { - content: "\e206"; -} -.glyphicon-alert:before { - content: "\e209"; -} -.glyphicon-equalizer:before { - content: "\e210"; -} -.glyphicon-king:before { - content: "\e211"; -} -.glyphicon-queen:before { - content: "\e212"; -} -.glyphicon-pawn:before { - content: "\e213"; -} -.glyphicon-bishop:before { - content: "\e214"; -} -.glyphicon-knight:before { - content: "\e215"; -} -.glyphicon-baby-formula:before { - content: "\e216"; -} -.glyphicon-tent:before { - content: "\26fa"; -} -.glyphicon-blackboard:before { - content: "\e218"; -} -.glyphicon-bed:before { - content: "\e219"; -} -.glyphicon-apple:before { - content: "\f8ff"; -} -.glyphicon-erase:before { - content: "\e221"; -} -.glyphicon-hourglass:before { - content: "\231b"; -} -.glyphicon-lamp:before { - content: "\e223"; -} -.glyphicon-duplicate:before { - content: "\e224"; -} -.glyphicon-piggy-bank:before { - content: "\e225"; -} -.glyphicon-scissors:before { - content: "\e226"; -} -.glyphicon-bitcoin:before { - content: "\e227"; -} -.glyphicon-btc:before { - content: "\e227"; -} -.glyphicon-xbt:before { - content: "\e227"; -} -.glyphicon-yen:before { - content: "\00a5"; -} -.glyphicon-jpy:before { - content: "\00a5"; -} -.glyphicon-ruble:before { - content: "\20bd"; -} -.glyphicon-rub:before { - content: "\20bd"; -} -.glyphicon-scale:before { - content: "\e230"; -} -.glyphicon-ice-lolly:before { - content: "\e231"; -} -.glyphicon-ice-lolly-tasted:before { - content: "\e232"; -} -.glyphicon-education:before { - content: "\e233"; -} -.glyphicon-option-horizontal:before { - content: "\e234"; -} -.glyphicon-option-vertical:before { - content: "\e235"; -} -.glyphicon-menu-hamburger:before { - content: "\e236"; -} -.glyphicon-modal-window:before { - content: "\e237"; -} -.glyphicon-oil:before { - content: "\e238"; -} -.glyphicon-grain:before { - content: "\e239"; -} -.glyphicon-sunglasses:before { - content: "\e240"; -} -.glyphicon-text-size:before { - content: "\e241"; -} -.glyphicon-text-color:before { - content: "\e242"; -} -.glyphicon-text-background:before { - content: "\e243"; -} -.glyphicon-object-align-top:before { - content: "\e244"; -} -.glyphicon-object-align-bottom:before { - content: "\e245"; -} -.glyphicon-object-align-horizontal:before { - content: "\e246"; -} -.glyphicon-object-align-left:before { - content: "\e247"; -} -.glyphicon-object-align-vertical:before { - content: "\e248"; -} -.glyphicon-object-align-right:before { - content: "\e249"; -} -.glyphicon-triangle-right:before { - content: "\e250"; -} -.glyphicon-triangle-left:before { - content: "\e251"; -} -.glyphicon-triangle-bottom:before { - content: "\e252"; -} -.glyphicon-triangle-top:before { - content: "\e253"; -} -.glyphicon-console:before { - content: "\e254"; -} -.glyphicon-superscript:before { - content: "\e255"; -} -.glyphicon-subscript:before { - content: "\e256"; -} -.glyphicon-menu-left:before { - content: "\e257"; -} -.glyphicon-menu-right:before { - content: "\e258"; -} -.glyphicon-menu-down:before { - content: "\e259"; -} -.glyphicon-menu-up:before { - content: "\e260"; -} diff --git a/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.min.css b/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.min.css deleted file mode 100644 index 651b6db5..00000000 --- a/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/css/bootstrap.min.css +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -/*! - * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=76290a4e23bf2c3f61a4e17625be4627) - * Config saved to config.json and https://gist.github.com/76290a4e23bf2c3f61a4e17625be4627 - *//*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"} diff --git a/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.eot b/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a4953fff68df523aa7656497ee339d6026d64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20127 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.ttf b/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc609ab6f21774de0cb7e01360095584f65b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45404 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 G`u_lkUM?>H diff --git a/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.woff2 b/src/Rules.Framework.WebUI/node_modules/glyphicons-only-bootstrap/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54c3751a6d9adb44c8e3a45ba5a73b77f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18028 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 diff --git a/src/Rules.Framework.WebUI/node_modules/jquery/dist/jquery.min.js b/src/Rules.Framework.WebUI/node_modules/jquery/dist/jquery.min.js deleted file mode 100644 index 2c69bc90..00000000 --- a/src/Rules.Framework.WebUI/node_modules/jquery/dist/jquery.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0a{min-width:30px;height:28px;line-height:28px;display:block;background:#fff;font-size:14px;color:#333;text-decoration:none;text-align:center}.paginationjs .paginationjs-pages li>a:hover{background:#eee}.paginationjs .paginationjs-pages li.active{border:none}.paginationjs .paginationjs-pages li.active>a{height:30px;line-height:30px;background:#aaa;color:#fff}.paginationjs .paginationjs-pages li.disabled>a{opacity:.3}.paginationjs .paginationjs-pages li.disabled>a:hover{background:0 0}.paginationjs .paginationjs-pages li:first-child,.paginationjs .paginationjs-pages li:first-child>a{border-radius:3px 0 0 3px}.paginationjs .paginationjs-pages li:last-child{border-right:1px solid #aaa;border-radius:0 3px 3px 0}.paginationjs .paginationjs-pages li:last-child>a{border-radius:0 3px 3px 0}.paginationjs .paginationjs-go-input>input[type=text]{width:30px;height:28px;background:#fff;border-radius:3px;border:1px solid #aaa;padding:0;font-size:14px;text-align:center;vertical-align:baseline;outline:0;box-shadow:none;box-sizing:initial}.paginationjs .paginationjs-go-button>input[type=button]{min-width:40px;height:30px;line-height:28px;background:#fff;border-radius:3px;border:1px solid #aaa;text-align:center;padding:0 8px;font-size:14px;vertical-align:baseline;outline:0;box-shadow:none;color:#333;cursor:pointer;vertical-align:middle\9}.paginationjs.paginationjs-theme-blue .paginationjs-go-input>input[type=text],.paginationjs.paginationjs-theme-blue .paginationjs-pages li{border-color:#289de9}.paginationjs .paginationjs-go-button>input[type=button]:hover{background-color:#f8f8f8}.paginationjs .paginationjs-nav{height:30px;line-height:30px}.paginationjs .paginationjs-go-button,.paginationjs .paginationjs-go-input{margin-left:5px\9}.paginationjs.paginationjs-small{font-size:12px}.paginationjs.paginationjs-small .paginationjs-pages li>a{min-width:26px;height:24px;line-height:24px;font-size:12px}.paginationjs.paginationjs-small .paginationjs-pages li.active>a{height:26px;line-height:26px}.paginationjs.paginationjs-small .paginationjs-go-input{font-size:12px}.paginationjs.paginationjs-small .paginationjs-go-input>input[type=text]{width:26px;height:24px;font-size:12px}.paginationjs.paginationjs-small .paginationjs-go-button{font-size:12px}.paginationjs.paginationjs-small .paginationjs-go-button>input[type=button]{min-width:30px;height:26px;line-height:24px;padding:0 6px;font-size:12px}.paginationjs.paginationjs-small .paginationjs-nav{height:26px;line-height:26px;font-size:12px}.paginationjs.paginationjs-big{font-size:16px}.paginationjs.paginationjs-big .paginationjs-pages li>a{min-width:36px;height:34px;line-height:34px;font-size:16px}.paginationjs.paginationjs-big .paginationjs-pages li.active>a{height:36px;line-height:36px}.paginationjs.paginationjs-big .paginationjs-go-input{font-size:16px}.paginationjs.paginationjs-big .paginationjs-go-input>input[type=text]{width:36px;height:34px;font-size:16px}.paginationjs.paginationjs-big .paginationjs-go-button{font-size:16px}.paginationjs.paginationjs-big .paginationjs-go-button>input[type=button]{min-width:50px;height:36px;line-height:34px;padding:0 12px;font-size:16px}.paginationjs.paginationjs-big .paginationjs-nav{height:36px;line-height:36px;font-size:16px}.paginationjs.paginationjs-theme-blue .paginationjs-pages li>a{color:#289de9}.paginationjs.paginationjs-theme-blue .paginationjs-pages li>a:hover{background:#e9f4fc}.paginationjs.paginationjs-theme-blue .paginationjs-pages li.active>a{background:#289de9;color:#fff}.paginationjs.paginationjs-theme-blue .paginationjs-pages li.disabled>a:hover{background:0 0}.paginationjs.paginationjs-theme-blue .paginationjs-go-button>input[type=button]{background:#289de9;border-color:#289de9;color:#fff}.paginationjs.paginationjs-theme-green .paginationjs-go-input>input[type=text],.paginationjs.paginationjs-theme-green .paginationjs-pages li{border-color:#449d44}.paginationjs.paginationjs-theme-blue .paginationjs-go-button>input[type=button]:hover{background-color:#3ca5ea}.paginationjs.paginationjs-theme-green .paginationjs-pages li>a{color:#449d44}.paginationjs.paginationjs-theme-green .paginationjs-pages li>a:hover{background:#ebf4eb}.paginationjs.paginationjs-theme-green .paginationjs-pages li.active>a{background:#449d44;color:#fff}.paginationjs.paginationjs-theme-green .paginationjs-pages li.disabled>a:hover{background:0 0}.paginationjs.paginationjs-theme-green .paginationjs-go-button>input[type=button]{background:#449d44;border-color:#449d44;color:#fff}.paginationjs.paginationjs-theme-yellow .paginationjs-go-input>input[type=text],.paginationjs.paginationjs-theme-yellow .paginationjs-pages li{border-color:#ec971f}.paginationjs.paginationjs-theme-green .paginationjs-go-button>input[type=button]:hover{background-color:#55a555}.paginationjs.paginationjs-theme-yellow .paginationjs-pages li>a{color:#ec971f}.paginationjs.paginationjs-theme-yellow .paginationjs-pages li>a:hover{background:#fdf5e9}.paginationjs.paginationjs-theme-yellow .paginationjs-pages li.active>a{background:#ec971f;color:#fff}.paginationjs.paginationjs-theme-yellow .paginationjs-pages li.disabled>a:hover{background:0 0}.paginationjs.paginationjs-theme-yellow .paginationjs-go-button>input[type=button]{background:#ec971f;border-color:#ec971f;color:#fff}.paginationjs.paginationjs-theme-red .paginationjs-go-input>input[type=text],.paginationjs.paginationjs-theme-red .paginationjs-pages li{border-color:#c9302c}.paginationjs.paginationjs-theme-yellow .paginationjs-go-button>input[type=button]:hover{background-color:#eea135}.paginationjs.paginationjs-theme-red .paginationjs-pages li>a{color:#c9302c}.paginationjs.paginationjs-theme-red .paginationjs-pages li>a:hover{background:#faeaea}.paginationjs.paginationjs-theme-red .paginationjs-pages li.active>a{background:#c9302c;color:#fff}.paginationjs.paginationjs-theme-red .paginationjs-pages li.disabled>a:hover{background:0 0}.paginationjs.paginationjs-theme-red .paginationjs-go-button>input[type=button]{background:#c9302c;border-color:#c9302c;color:#fff}.paginationjs.paginationjs-theme-red .paginationjs-go-button>input[type=button]:hover{background-color:#ce4541}.paginationjs .paginationjs-pages li.paginationjs-next{border-right:1px solid #aaa\9}.paginationjs .paginationjs-go-input>input[type=text]{line-height:28px\9;vertical-align:middle\9}.paginationjs.paginationjs-big .paginationjs-pages li>a{line-height:36px\9}.paginationjs.paginationjs-big .paginationjs-go-input>input[type=text]{height:36px\9;line-height:36px\9} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/node_modules/paginationjs/dist/pagination.min.js b/src/Rules.Framework.WebUI/node_modules/paginationjs/dist/pagination.min.js deleted file mode 100644 index c8ed6f1b..00000000 --- a/src/Rules.Framework.WebUI/node_modules/paginationjs/dist/pagination.min.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * pagination.js 2.1.5 - * A jQuery plugin to provide simple yet fully customisable pagination - * https://github.com/superRaytin/paginationjs - - * Homepage: http://pagination.js.org - * - * Copyright 2014-2100, superRaytin - * Released under the MIT license. -*/ -!function(a,b){function c(a){throw new Error("Pagination: "+a)}function d(a){a.dataSource||c('"dataSource" is required.'),"string"==typeof a.dataSource?void 0===a.totalNumberLocator?void 0===a.totalNumber?c('"totalNumber" is required.'):b.isNumeric(a.totalNumber)||c('"totalNumber" is incorrect. (Number)'):b.isFunction(a.totalNumberLocator)||c('"totalNumberLocator" should be a Function.'):i.isObject(a.dataSource)&&(void 0===a.locator?c('"dataSource" is an Object, please specify "locator".'):"string"==typeof a.locator||b.isFunction(a.locator)||c(a.locator+" is incorrect. (String | Function)")),void 0===a.formatResult||b.isFunction(a.formatResult)||c('"formatResult" should be a Function.')}function e(a){var c=["go","previous","next","disable","enable","refresh","show","hide","destroy"];b.each(c,function(b,c){a.off(h+c)}),a.data("pagination",{}),b(".paginationjs",a).remove()}function f(a,b){return("object"==(b=typeof a)?null==a&&"null"||Object.prototype.toString.call(a).slice(8,-1):b).toLowerCase()}void 0===b&&c("Pagination requires jQuery.");var g="pagination",h="__pagination-";b.fn.pagination&&(g="pagination2"),b.fn[g]=function(f){if(void 0===f)return this;var j=b(this),k=b.extend({},b.fn[g].defaults,f),l={initialize:function(){var a=this;if(j.data("pagination")||j.data("pagination",{}),!1!==a.callHook("beforeInit")){j.data("pagination").initialized&&b(".paginationjs",j).remove(),a.disabled=!!k.disabled;var c=a.model={pageRange:k.pageRange,pageSize:k.pageSize};a.parseDataSource(k.dataSource,function(b){a.isAsync=i.isString(b),i.isArray(b)&&(c.totalNumber=k.totalNumber=b.length),a.isDynamicTotalNumber=a.isAsync&&k.totalNumberLocator;var d=a.render(!0);k.className&&d.addClass(k.className),c.el=d,j["bottom"===k.position?"append":"prepend"](d),a.observer(),j.data("pagination").initialized=!0,a.callHook("afterInit",d)})}},render:function(a){var c=this,d=c.model,e=d.el||b('
'),f=!0!==a;c.callHook("beforeRender",f);var g=d.pageNumber||k.pageNumber,h=k.pageRange||0,i=c.getTotalPage(),j=g-h,l=g+h;return l>i&&(l=i,j=i-2*h,j=j<1?1:j),j<=1&&(j=1,l=Math.min(2*h+1,i)),e.html(c.generateHTML({currentPage:g,pageRange:h,rangeStart:j,rangeEnd:l})),k.hideWhenLessThanOnePage&&e[i<=1?"hide":"show"](),c.callHook("afterRender",f),e},generatePageNumbersHTML:function(a){var b,c=this,d=a.currentPage,e=c.getTotalPage(),f=a.rangeStart,g=a.rangeEnd,h="",i=k.pageLink,j=k.ellipsisText,l=k.classPrefix,m=k.activeClassName,n=k.disableClassName;if(null===k.pageRange){for(b=1;b<=e;b++)h+=b==d?'
  • '+b+"
  • ":'
  • '+b+"
  • ";return h}if(f<=3)for(b=1;b'+b+"":'
  • '+b+"
  • ";else k.showFirstOnEllipsisShow&&(h+='
  • 1
  • '),h+='
  • '+j+"
  • ";for(b=f;b<=g;b++)h+=b==d?'
  • '+b+"
  • ":'
  • '+b+"
  • ";if(g>=e-2)for(b=g+1;b<=e;b++)h+='
  • '+b+"
  • ";else h+='
  • '+j+"
  • ",k.showLastOnEllipsisShow&&(h+='
  • '+e+"
  • ");return h},generateHTML:function(a){var c,d=this,e=a.currentPage,f=d.getTotalPage(),g=d.getTotalNumber(),h=k.showPrevious,i=k.showNext,j=k.showPageNumbers,l=k.showNavigator,m=k.showGoInput,n=k.showGoButton,o=k.pageLink,p=k.prevText,q=k.nextText,r=k.goButtonText,s=k.classPrefix,t=k.disableClassName,u=k.ulClassName,v="",w='',x='',y=b.isFunction(k.formatNavigator)?k.formatNavigator(e,f,g):k.formatNavigator,z=b.isFunction(k.formatGoInput)?k.formatGoInput(w,e,f,g):k.formatGoInput,A=b.isFunction(k.formatGoButton)?k.formatGoButton(x,e,f,g):k.formatGoButton,B=b.isFunction(k.autoHidePrevious)?k.autoHidePrevious():k.autoHidePrevious,C=b.isFunction(k.autoHideNext)?k.autoHideNext():k.autoHideNext,D=b.isFunction(k.header)?k.header(e,f,g):k.header,E=b.isFunction(k.footer)?k.footer(e,f,g):k.footer;return D&&(c=d.replaceVariables(D,{currentPage:e,totalPage:f,totalNumber:g}),v+=c),(h||j||i)&&(v+='
    ',v+=u?'
      ':"
        ",h&&(e<=1?B||(v+='
      • '+p+"
      • "):v+='
      • '+p+"
      • "),j&&(v+=d.generatePageNumbersHTML(a)),i&&(e>=f?C||(v+='
      • '+q+"
      • "):v+='
      • '+q+"
      • "),v+="
    "),l&&y&&(c=d.replaceVariables(y,{currentPage:e,totalPage:f,totalNumber:g}),v+='
    '+c+"
    "),m&&z&&(c=d.replaceVariables(z,{currentPage:e,totalPage:f,totalNumber:g,input:w}),v+='
    '+c+"
    "),n&&A&&(c=d.replaceVariables(A,{currentPage:e,totalPage:f,totalNumber:g,button:x}),v+='
    '+c+"
    "),E&&(c=d.replaceVariables(E,{currentPage:e,totalPage:f,totalNumber:g}),v+=c),v},findTotalNumberFromRemoteResponse:function(a){this.model.totalNumber=k.totalNumberLocator(a)},go:function(a,c){function d(a){if(!1===e.callHook("beforePaging",g))return!1;if(f.direction=void 0===f.pageNumber?0:g>f.pageNumber?1:-1,f.pageNumber=g,e.render(),e.disabled&&e.isAsync&&e.enable(),j.data("pagination").model=f,k.formatResult){var d=b.extend(!0,[],a);i.isArray(a=k.formatResult(d))||(a=d)}j.data("pagination").currentPageData=a,e.doCallback(a,c),e.callHook("afterPaging",g),1==g&&e.callHook("afterIsFirstPage"),g==e.getTotalPage()&&e.callHook("afterIsLastPage")}var e=this,f=e.model;if(!e.disabled){var g=a;if((g=parseInt(g))&&!(g<1)){var h=k.pageSize,l=e.getTotalNumber(),m=e.getTotalPage();if(!(l>0&&g>m)){if(!e.isAsync)return void d(e.getDataFragment(g));var n={},o=k.alias||{};n[o.pageSize?o.pageSize:"pageSize"]=h,n[o.pageNumber?o.pageNumber:"pageNumber"]=g;var p=b.isFunction(k.ajax)?k.ajax():k.ajax,q={type:"get",cache:!1,data:{},contentType:"application/x-www-form-urlencoded; charset=UTF-8",dataType:"json",async:!0};b.extend(!0,q,p),b.extend(q.data,n),q.url=k.dataSource,q.success=function(a){e.isDynamicTotalNumber?e.findTotalNumberFromRemoteResponse(a):e.model.totalNumber=k.totalNumber,d(e.filterDataByLocator(a))},q.error=function(a,b,c){k.formatAjaxError&&k.formatAjaxError(a,b,c),e.enable()},e.disable(),b.ajax(q)}}}},doCallback:function(a,c){var d=this,e=d.model;b.isFunction(c)?c(a,e):b.isFunction(k.callback)&&k.callback(a,e)},destroy:function(){!1!==this.callHook("beforeDestroy")&&(this.model.el.remove(),j.off(),b("#paginationjs-style").remove(),this.callHook("afterDestroy"))},previous:function(a){this.go(this.model.pageNumber-1,a)},next:function(a){this.go(this.model.pageNumber+1,a)},disable:function(){var a=this,b=a.isAsync?"async":"sync";!1!==a.callHook("beforeDisable",b)&&(a.disabled=!0,a.model.disabled=!0,a.callHook("afterDisable",b))},enable:function(){var a=this,b=a.isAsync?"async":"sync";!1!==a.callHook("beforeEnable",b)&&(a.disabled=!1,a.model.disabled=!1,a.callHook("afterEnable",b))},refresh:function(a){this.go(this.model.pageNumber,a)},show:function(){var a=this;a.model.el.is(":visible")||a.model.el.show()},hide:function(){var a=this;a.model.el.is(":visible")&&a.model.el.hide()},replaceVariables:function(a,b){var c;for(var d in b){var e=b[d],f=new RegExp("<%=\\s*"+d+"\\s*%>","img");c=(c||a).replace(f,e)}return c},getDataFragment:function(a){var b=k.pageSize,c=k.dataSource,d=this.getTotalNumber(),e=b*(a-1)+1,f=Math.min(a*b,d);return c.slice(e-1,f)},getTotalNumber:function(){return this.model.totalNumber||k.totalNumber||0},getTotalPage:function(){return Math.ceil(this.getTotalNumber()/k.pageSize)},getLocator:function(a){var d;return"string"==typeof a?d=a:b.isFunction(a)?d=a():c('"locator" is incorrect. (String | Function)'),d},filterDataByLocator:function(a){var d,e=this.getLocator(k.locator);if(i.isObject(a)){try{b.each(e.split("."),function(b,c){d=(d||a)[c]})}catch(a){}d?i.isArray(d)||c("dataSource."+e+" must be an Array."):c("dataSource."+e+" is undefined.")}return d||a},parseDataSource:function(a,d){var e=this;i.isObject(a)?d(k.dataSource=e.filterDataByLocator(a)):i.isArray(a)?d(k.dataSource=a):b.isFunction(a)?k.dataSource(function(a){i.isArray(a)||c('The parameter of "done" Function should be an Array.'),e.parseDataSource.call(e,a,d)}):"string"==typeof a?(/^https?|file:/.test(a)&&(k.ajaxDataType="jsonp"),d(a)):c('Unexpected type of "dataSource".')},callHook:function(c){var d,e=j.data("pagination"),f=Array.prototype.slice.apply(arguments);return f.shift(),k[c]&&b.isFunction(k[c])&&!1===k[c].apply(a,f)&&(d=!1),e.hooks&&e.hooks[c]&&b.each(e.hooks[c],function(b,c){!1===c.apply(a,f)&&(d=!1)}),!1!==d},observer:function(){var a=this,d=a.model.el;j.on(h+"go",function(d,e,f){(e=parseInt(b.trim(e)))&&(b.isNumeric(e)||c('"pageNumber" is incorrect. (Number)'),a.go(e,f))}),d.delegate(".J-paginationjs-page","click",function(c){var d=b(c.currentTarget),e=b.trim(d.attr("data-num"));if(e&&!d.hasClass(k.disableClassName)&&!d.hasClass(k.activeClassName))return!1!==a.callHook("beforePageOnClick",c,e)&&(a.go(e),a.callHook("afterPageOnClick",c,e),!!k.pageLink&&void 0)}),d.delegate(".J-paginationjs-previous","click",function(c){var d=b(c.currentTarget),e=b.trim(d.attr("data-num"));if(e&&!d.hasClass(k.disableClassName))return!1!==a.callHook("beforePreviousOnClick",c,e)&&(a.go(e),a.callHook("afterPreviousOnClick",c,e),!!k.pageLink&&void 0)}),d.delegate(".J-paginationjs-next","click",function(c){var d=b(c.currentTarget),e=b.trim(d.attr("data-num"));if(e&&!d.hasClass(k.disableClassName))return!1!==a.callHook("beforeNextOnClick",c,e)&&(a.go(e),a.callHook("afterNextOnClick",c,e),!!k.pageLink&&void 0)}),d.delegate(".J-paginationjs-go-button","click",function(c){var e=b(".J-paginationjs-go-pagenumber",d).val();if(!1===a.callHook("beforeGoButtonOnClick",c,e))return!1;j.trigger(h+"go",e),a.callHook("afterGoButtonOnClick",c,e)}),d.delegate(".J-paginationjs-go-pagenumber","keyup",function(c){if(13===c.which){var e=b(c.currentTarget).val();if(!1===a.callHook("beforeGoInputOnEnter",c,e))return!1;j.trigger(h+"go",e),b(".J-paginationjs-go-pagenumber",d).focus(),a.callHook("afterGoInputOnEnter",c,e)}}),j.on(h+"previous",function(b,c){a.previous(c)}),j.on(h+"next",function(b,c){a.next(c)}),j.on(h+"disable",function(){a.disable()}),j.on(h+"enable",function(){a.enable()}),j.on(h+"refresh",function(b,c){a.refresh(c)}),j.on(h+"show",function(){a.show()}),j.on(h+"hide",function(){a.hide()}),j.on(h+"destroy",function(){a.destroy()});var e=Math.max(a.getTotalPage(),1),f=k.pageNumber;a.isDynamicTotalNumber&&(f=1),k.triggerPagingOnInit&&j.trigger(h+"go",Math.min(f,e))}};if(j.data("pagination")&&!0===j.data("pagination").initialized){if(b.isNumeric(f))return j.trigger.call(this,h+"go",f,arguments[1]),this;if("string"==typeof f){var m=Array.prototype.slice.apply(arguments);switch(m[0]=h+m[0],f){case"previous":case"next":case"go":case"disable":case"enable":case"refresh":case"show":case"hide":case"destroy":j.trigger.apply(this,m);break;case"getSelectedPageNum":return j.data("pagination").model?j.data("pagination").model.pageNumber:j.data("pagination").attributes.pageNumber;case"getTotalPage":return Math.ceil(j.data("pagination").model.totalNumber/j.data("pagination").model.pageSize);case"getSelectedPageData":return j.data("pagination").currentPageData;case"isDisabled":return!0===j.data("pagination").model.disabled;default:c("Unknown action: "+f)}return this}e(j)}else i.isObject(f)||c("Illegal options");return d(k),l.initialize(),this},b.fn[g].defaults={totalNumber:0,pageNumber:1,pageSize:10,pageRange:2,showPrevious:!0,showNext:!0,showPageNumbers:!0,showNavigator:!1,showGoInput:!1,showGoButton:!1,pageLink:"",prevText:"«",nextText:"»",ellipsisText:"...",goButtonText:"Go",classPrefix:"paginationjs",activeClassName:"active",disableClassName:"disabled",inlineStyle:!0,formatNavigator:"<%= currentPage %> / <%= totalPage %>",formatGoInput:"<%= input %>",formatGoButton:"<%= button %>",position:"bottom",autoHidePrevious:!1,autoHideNext:!1,triggerPagingOnInit:!0,hideWhenLessThanOnePage:!1,showFirstOnEllipsisShow:!0,showLastOnEllipsisShow:!0,callback:function(){}},b.fn.addHook=function(a,d){arguments.length<2&&c("Missing argument."),b.isFunction(d)||c("callback must be a function.");var e=b(this),f=e.data("pagination");f||(e.data("pagination",{}),f=e.data("pagination")),!f.hooks&&(f.hooks={}),f.hooks[a]=f.hooks[a]||[],f.hooks[a].push(d)},b[g]=function(a,d){arguments.length<2&&c("Requires two parameters.");var e;if(e="string"!=typeof a&&a instanceof jQuery?a:b(a),e.length)return e.pagination(d),e};var i={};b.each(["Object","Array","String"],function(a,b){i["is"+b]=function(a){return f(a)===b.toLowerCase()}}),"function"==typeof define&&define.amd&&define(function(){return b})}(this,window.jQuery); \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/node_modules/rules_list.ico b/src/Rules.Framework.WebUI/node_modules/rules_list.ico deleted file mode 100644 index 4b4d5da76d7818b61cef8c5ce1464ee67d2ac66f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6926 zcmZQzU<5)fbp{}*!0Trg9U)Z|4}d+0;3^7 zlMpE0_x?WvgC!6%fN`+}FuT3p3%2VTkd*M91DAuTp_E3pgIG^7G#$fm$N%FkVC_pE z{|CwiVaQYJPLO_>9i#4qqz_8Nl+^GXD(REh^f9WAxDdGa`TzeZ%l7{7>FW7E1&HtM zTME?p9j(); - } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Handlers/GetConfigurationsHandlerTests.cs b/tests/Rules.Framework.WebUI.Tests/Handlers/GetConfigurationsHandlerTests.cs deleted file mode 100644 index f29487f7..00000000 --- a/tests/Rules.Framework.WebUI.Tests/Handlers/GetConfigurationsHandlerTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Rules.Framework.WebUI.Tests.Handlers -{ - using System.IO; - using System.Net; - using System.Threading.Tasks; - using FluentAssertions; - using Microsoft.AspNetCore.Http; - using Moq; - using Rules.Framework.WebUI.Handlers; - using Rules.Framework.WebUI.Tests.Utilities; - using Xunit; - - public class GetConfigurationsHandlerTests - { - private readonly GetConfigurationsHandler handler; - private readonly Mock rulesEngine; - - public GetConfigurationsHandlerTests() - { - this.rulesEngine = new Mock(); - this.rulesEngine - .SetupGet(x => x.Options) - .Returns(RulesEngineOptions.NewWithDefaults()); - this.handler = new GetConfigurationsHandler(rulesEngine.Object, new WebUIOptions()); - } - - [Theory] - [InlineData("POST", "/rules/api/v1/configurations", false)] - [InlineData("GET", "/rules/api/v1/rulesets", false)] - [InlineData("GET", "/rules/api/v1/configurations", true)] - public async Task HandleRequestAsync_Validation(string httpMethod, string resourcePath, - bool expectedResult) - { - //Arrange - var httpContext = HttpContextHelper.CreateHttpContext(httpMethod, resourcePath); - RequestDelegate next = (HttpContext _) => Task.CompletedTask; - - //Act - var result = await this.handler.HandleAsync(httpContext.Request, httpContext.Response, next); - - //Assert - result.Should().Be(expectedResult); - if (expectedResult) - { - httpContext.Response.Should().NotBeNull(); - httpContext.Response.StatusCode.Should().Be((int)HttpStatusCode.OK); - httpContext.Response.ContentType.Should().Be("application/json"); - var body = string.Empty; - using (var reader = new StreamReader(httpContext.Response.Body)) - { - httpContext.Response.Body.Seek(0, SeekOrigin.Begin); - body = await reader.ReadToEndAsync(); - } - body.Should().NotBeNullOrWhiteSpace(); - httpContext.Response.ContentLength.Should().Be(body.Length); - this.rulesEngine.Verify(s => s.Options, Times.AtLeastOnce()); - } - else - { - this.rulesEngine.Verify(s => s.Options, Times.Never()); - } - } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Handlers/GetIndexPageHandlerTests.cs b/tests/Rules.Framework.WebUI.Tests/Handlers/GetIndexPageHandlerTests.cs deleted file mode 100644 index 749b1a62..00000000 --- a/tests/Rules.Framework.WebUI.Tests/Handlers/GetIndexPageHandlerTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Rules.Framework.WebUI.Tests.Handlers -{ - using System.Threading.Tasks; - using FluentAssertions; - using Microsoft.AspNetCore.Http; - using Rules.Framework.WebUI.Handlers; - using Rules.Framework.WebUI.Tests.Utilities; - using Xunit; - - public class GetIndexPageHandlerTests - { - private readonly GetIndexPageHandler handler; - - public GetIndexPageHandlerTests() - { - this.handler = new GetIndexPageHandler(new WebUIOptions()); - } - - [Theory] - [InlineData("POST", "/rules/index.html", false)] - [InlineData("GET", "/rules/Rule/List", false)] - [InlineData("GET", "/rules/index.html", true)] - [InlineData("GET", "/rules", true)] - public async Task HandleRequestAsync_Validation(string httpMethod, string resourcePath, - bool expectedResult) - { - //Arrange - var httpContext = HttpContextHelper.CreateHttpContext(httpMethod, resourcePath); - RequestDelegate next = (HttpContext _) => Task.CompletedTask; - - //Act - var result = await this.handler - .HandleAsync(httpContext.Request, httpContext.Response, next) - .ConfigureAwait(false); - - //Assert - result.Should().Be(expectedResult); - } - } -} \ 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 deleted file mode 100644 index 66e42fcf..00000000 --- a/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesHandlerTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -namespace Rules.Framework.WebUI.Tests.Handlers -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Threading.Tasks; - using FluentAssertions; - using Microsoft.AspNetCore.Http; - using Moq; - using Rules.Framework.WebUI.Dto; - using Rules.Framework.WebUI.Handlers; - using Rules.Framework.WebUI.Tests.Utilities; - using Xunit; - - public class GetRulesHandlerTests - { - private readonly GetRulesHandler handler; - private readonly Mock rulesEngine; - - public GetRulesHandlerTests() - { - this.rulesEngine = new Mock(); - this.rulesEngine.SetupGet(x => x.Options) - .Returns(RulesEngineOptions.NewWithDefaults()); - var ruleStatusDtoAnalyzer = new RuleStatusDtoAnalyzer(); - this.handler = new GetRulesHandler(rulesEngine.Object, ruleStatusDtoAnalyzer, new WebUIOptions()); - } - - [Theory] - [InlineData("POST", "/rules/api/v1/rules", false, null)] - [InlineData("GET", "/rules/api/v1/rulesets", false, null)] - [InlineData("GET", "/rules/api/v1/rules", true, HttpStatusCode.OK)] - [InlineData("GET", "/rules/api/v1/rules", true, HttpStatusCode.InternalServerError)] - public async Task HandleRequestAsync_Validation(string httpMethod, string resourcePath, - bool expectedResult, HttpStatusCode? statusCode) - { - //Arrange - var httpContext = HttpContextHelper.CreateHttpContext(httpMethod, resourcePath); - var genericRule = new List(); - var verifySearchAsync = false; - - if (statusCode == HttpStatusCode.OK || statusCode == HttpStatusCode.InternalServerError) - { - verifySearchAsync = true; - - httpContext.Request.QueryString = new QueryString("?ruleset=1"); - - if (statusCode == HttpStatusCode.OK) - { - this.rulesEngine.Setup(d => d.SearchAsync(It.IsAny>())) - .ReturnsAsync(genericRule); - } - - if (statusCode == HttpStatusCode.InternalServerError) - { - this.rulesEngine.Setup(d => d.SearchAsync(It.IsAny>())) - .Throws(new Exception("message", new Exception("inner"))); - } - } - else - { - httpContext.Request.QueryString = new QueryString(); - } - RequestDelegate next = (HttpContext _) => Task.CompletedTask; - - //Act - var result = await this.handler.HandleAsync(httpContext.Request, httpContext.Response, next); - - //Assert - result.Should().Be(expectedResult); - if (expectedResult) - { - httpContext.Response.Should().NotBeNull(); - httpContext.Response.StatusCode.Should().Be((int)statusCode); - httpContext.Response.ContentType.Should().Be("application/json"); - var body = string.Empty; - using (var reader = new StreamReader(httpContext.Response.Body)) - { - httpContext.Response.Body.Seek(0, SeekOrigin.Begin); - body = await reader.ReadToEndAsync(); - } - body.Should().NotBeNullOrWhiteSpace(); - httpContext.Response.ContentLength.Should().Be(body.Length); - } - - if (verifySearchAsync) - { - this.rulesEngine - .Verify(s => s.SearchAsync(It.IsAny>()), Times.Once); - } - else - { - this.rulesEngine - .Verify(s => s.SearchAsync(It.IsAny>()), Times.Never); - } - } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesetsHandlerTests.cs b/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesetsHandlerTests.cs deleted file mode 100644 index 03104a1c..00000000 --- a/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesetsHandlerTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Rules.Framework.WebUI.Tests.Handlers -{ - using System.IO; - using System.Net; - using System.Threading.Tasks; - using FluentAssertions; - using Microsoft.AspNetCore.Http; - using Moq; - using Rules.Framework.WebUI.Dto; - using Rules.Framework.WebUI.Handlers; - using Rules.Framework.WebUI.Tests.Utilities; - using Xunit; - - public class GetRulesetsHandlerTests - { - private readonly GetRulesetsHandler handler; - private readonly Mock rulesEngine; - - public GetRulesetsHandlerTests() - { - var ruleStatusDtoAnalyzer = new RuleStatusDtoAnalyzer(); - this.rulesEngine = new Mock(); - this.handler = new GetRulesetsHandler(rulesEngine.Object, ruleStatusDtoAnalyzer, new WebUIOptions()); - } - - [Theory] - [InlineData("POST", "/rules/api/v1/rulesets", false)] - [InlineData("GET", "/rules/api/v1/rules", false)] - [InlineData("GET", "/rules/api/v1/rulesets", true)] - public async Task HandleRequestAsync_Validation(string httpMethod, string resourcePath, - bool expectedResult) - { - //Arrange - var httpContext = HttpContextHelper.CreateHttpContext(httpMethod, resourcePath); - RequestDelegate next = (HttpContext _) => Task.CompletedTask; - //Act - var result = await this.handler.HandleAsync(httpContext.Request, httpContext.Response, next); - - //Assert - result.Should().Be(expectedResult); - if (expectedResult) - { - httpContext.Response.Should().NotBeNull(); - httpContext.Response.StatusCode.Should().Be((int)HttpStatusCode.OK); - httpContext.Response.ContentType.Should().Be("application/json"); - var body = string.Empty; - using (var reader = new StreamReader(httpContext.Response.Body)) - { - httpContext.Response.Body.Seek(0, SeekOrigin.Begin); - body = await reader.ReadToEndAsync(); - } - body.Should().NotBeNullOrWhiteSpace(); - httpContext.Response.ContentLength.Should().Be(body.Length); - this.rulesEngine.Verify(s => s.GetRulesetsAsync(), Times.Once); - } - else - { - this.rulesEngine.Verify(s => s.GetRulesetsAsync(), Times.Never); - } - } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj b/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj index 407f146e..5b0657fb 100644 --- a/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj +++ b/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj @@ -1,19 +1,19 @@ - - net8.0 - 9.0 - Full - false - + + net8.0 + 9.0 + Full + false + - - 10.0 - + + 10.0 + - - 10.0 - + + 10.0 + @@ -31,7 +31,7 @@ - - - + + + \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Services/GuidGeneratorTests.cs b/tests/Rules.Framework.WebUI.Tests/Services/GuidGeneratorTests.cs new file mode 100644 index 00000000..16f24223 --- /dev/null +++ b/tests/Rules.Framework.WebUI.Tests/Services/GuidGeneratorTests.cs @@ -0,0 +1,49 @@ +namespace Rules.Framework.WebUI.Tests.Services +{ + using System; + using FluentAssertions; + using Rules.Framework.WebUI.Services; + using Xunit; + + public class GuidGeneratorTests + { + [Theory] + [InlineData("")] + [InlineData(" ")] + public void GenerateFromString_GivenEmptyOrWhiteSpaceSourceString_ThrowsException(string source) + { + // Act + var actual = Assert.Throws(() => GuidGenerator.GenerateFromString(source)); + + // Assert + actual.Should().BeOfType(); + actual.ParamName.Should().Be("source"); + } + + [Fact] + public void GenerateFromString_GivenNullSourceString_ThrowsException() + { + // Act + var actual = Assert.Throws(() => GuidGenerator.GenerateFromString(null)); + + // Assert + actual.Should().BeOfType(); + actual.ParamName.Should().Be("source"); + } + + [Theory] + [InlineData("Test string", "849de4a3-f13d-2e3c-2a77-86f6ecd7e0d1")] + [InlineData("Yet another test string", "1ea74e0c-ac94-db72-ad40-e19920a0fa00")] + public void GenerateFromString_GivenSourceString_ReturnsGuid(string source, string expectedGuid) + { + // Arrange + var expected = Guid.Parse(expectedGuid); + + // Act + var actual = GuidGenerator.GenerateFromString(source); + + // Assert + actual.Should().Be(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Services/RulesEngineInstanceProviderTests.cs b/tests/Rules.Framework.WebUI.Tests/Services/RulesEngineInstanceProviderTests.cs new file mode 100644 index 00000000..38ec4218 --- /dev/null +++ b/tests/Rules.Framework.WebUI.Tests/Services/RulesEngineInstanceProviderTests.cs @@ -0,0 +1,100 @@ +namespace Rules.Framework.WebUI.Tests.Services +{ + using System; + using FluentAssertions; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using Rules.Framework.WebUI.Services; + using Xunit; + + public class RulesEngineInstanceProviderTests + { + [Fact] + public void RegisterOneInstanceTest_Success() + { + // Arrange + var name = "Sample engine"; + var instanceId = Guid.Parse("aeccf6a8-7851-cda0-4e2b-802a97707225"); + var rulesEngine = Mock.Of(); + + var serviceProvider = Mock.Of(); + Mock.Get(serviceProvider) + .Setup(x => x.GetService(typeof(IRulesEngine))) + .Returns(rulesEngine); + var rulesEngineInstanceProvider = new RulesEngineInstanceProvider(); + + // Act + rulesEngineInstanceProvider.AddInstance(name, (sp, _) => sp.GetService()); + rulesEngineInstanceProvider.EnumerateInstances(serviceProvider); + var instance = rulesEngineInstanceProvider.GetInstance(instanceId); + var instances = rulesEngineInstanceProvider.GetAllInstances(); + + // Assert + instance.Should().NotBeNull(); + instance.Id.Should().Be(instanceId); + instance.Name.Should().Be(name); + instance.RulesEngine.Should().BeSameAs(rulesEngine); + instances.Should().NotBeNull() + .And.HaveCount(1) + .And.Contain(instance); + } + + [Fact] + public void RegisterTwoInstanceSameNameTest_Failure() + { + // Arrange + var name = "Sample engine"; + + var rulesEngineInstanceProvider = new RulesEngineInstanceProvider(); + + // Act + rulesEngineInstanceProvider.AddInstance(name, (sp, _) => sp.GetService()); + var exception = Assert.Throws( + () => rulesEngineInstanceProvider.AddInstance(name, (sp, _) => sp.GetService())); + + // Assert + exception.Message.Should().Contain(name); + } + + [Fact] + public void RegisterTwoInstanceTest_Success() + { + // Arrange + var name1 = "Sample engine"; + var instanceId1 = Guid.Parse("aeccf6a8-7851-cda0-4e2b-802a97707225"); + var rulesEngine1 = Mock.Of(); + var name2 = "Another sample engine"; + var instanceId2 = Guid.Parse("1c45bfc8-7dfb-f399-adbf-0976e00d3e3e"); + var rulesEngine2 = Mock.Of(); + + var serviceProvider = Mock.Of(); + Mock.Get(serviceProvider) + .SetupSequence(x => x.GetService(typeof(IRulesEngine))) + .Returns(rulesEngine1) + .Returns(rulesEngine2); + var rulesEngineInstanceProvider = new RulesEngineInstanceProvider(); + + // Act + rulesEngineInstanceProvider.AddInstance(name1, (sp, _) => sp.GetService()); + rulesEngineInstanceProvider.AddInstance(name2, (sp, _) => sp.GetService()); + rulesEngineInstanceProvider.EnumerateInstances(serviceProvider); + var instance1 = rulesEngineInstanceProvider.GetInstance(instanceId1); + var instance2 = rulesEngineInstanceProvider.GetInstance(instanceId2); + var instances = rulesEngineInstanceProvider.GetAllInstances(); + + // Assert + instance1.Should().NotBeNull(); + instance1.Id.Should().Be(instanceId1); + instance1.Name.Should().Be(name1); + instance1.RulesEngine.Should().BeSameAs(rulesEngine1); + instance2.Should().NotBeNull(); + instance2.Id.Should().Be(instanceId2); + instance2.Name.Should().Be(name2); + instance2.RulesEngine.Should().BeSameAs(rulesEngine2); + instances.Should().NotBeNull() + .And.HaveCount(2) + .And.Contain(instance1) + .And.Contain(instance2); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Utilities/HttpContextHelper.cs b/tests/Rules.Framework.WebUI.Tests/Utilities/HttpContextHelper.cs deleted file mode 100644 index 9e061426..00000000 --- a/tests/Rules.Framework.WebUI.Tests/Utilities/HttpContextHelper.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Rules.Framework.WebUI.Tests.Utilities -{ - using System.IO; - using System.Text; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Newtonsoft.Json; - - internal static class HttpContextHelper - { - public static async Task CreateContext(string path, string method, object requestBody = null) - { - var context = new DefaultHttpContext(); - context.Request.Path = path; - context.Request.Method = method; - context.Request.ContentType = "application/json"; - - if (requestBody is not null) - { - var body = JsonConvert.SerializeObject(requestBody, - new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Utc }); - - using (var writer = new StreamWriter(context.Request.Body, Encoding.UTF8)) - { - await writer.WriteAsync(body).ConfigureAwait(false); - } - } - - context.Response.Body = new MemoryStream(); - - return context; - } - - public static HttpContext CreateDefaultContext() - { - return new DefaultHttpContext(); - } - - public static HttpContext CreateHttpContext(string httpMethod, string resourcePath) - { - var context = new DefaultHttpContext(); - - context.Request.Path = resourcePath; - context.Request.Method = httpMethod; - - context.Response.Body = new MemoryStream(); - - return context; - } - - public static async Task ReadResponse(HttpResponse response) - { - //Rewind the stream - response.Body.Seek(0, SeekOrigin.Begin); - - T responseDto; - - using (var reader = new StreamReader(response.Body, Encoding.UTF8)) - { - var requestMessage = await reader.ReadToEndAsync().ConfigureAwait(false); - - responseDto = JsonConvert.DeserializeObject(requestMessage, - new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Utc }); - } - - return responseDto; - } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Utilities/WebUIMiddlewareFactory.cs b/tests/Rules.Framework.WebUI.Tests/Utilities/WebUIMiddlewareFactory.cs deleted file mode 100644 index bc865304..00000000 --- a/tests/Rules.Framework.WebUI.Tests/Utilities/WebUIMiddlewareFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Rules.Framework.WebUI.Tests.Utilities -{ - using System.Collections.Generic; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Logging; - - public static class WebUIMiddlewareFactory - { - internal static WebUIMiddleware Create( - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - IEnumerable handlers) - { - return new WebUIMiddleware(loggerFactory: loggerFactory, hostingEnv: hostingEnv, - next: (_) => - { - return Task.CompletedTask; - }, - httpRequestHandlers: handlers, - options: new WebUIOptions()); - } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/ViewModels/RuleViewModelExtensionsTests.cs b/tests/Rules.Framework.WebUI.Tests/ViewModels/RuleViewModelExtensionsTests.cs new file mode 100644 index 00000000..e12426a0 --- /dev/null +++ b/tests/Rules.Framework.WebUI.Tests/ViewModels/RuleViewModelExtensionsTests.cs @@ -0,0 +1,125 @@ +namespace Rules.Framework.WebUI.Tests.ViewModels +{ + using System; + using FluentAssertions; + using Rules.Framework.WebUI.ViewModels; + using Xunit; + + public class RuleViewModelExtensionsTests + { + [Fact] + public void ToExportRulesModel_GivenRuleViewModel_ReturnsExportRulesModel() + { + // Arrange + var ruleViewModel = new RuleViewModel + { + Active = true, + Content = new object(), + DateBegin = DateTime.Parse("2024-10-01Z"), + DateEnd = DateTime.Parse("2024-10-31Z"), + Name = "Sample name", + Priority = 1, + RootCondition = new ComposedConditionNodeViewModel + { + ChildConditionNodes = new[] + { + new ValueConditionNodeViewModel + { + Condition = "Condition1", + DataType = "String", + LogicalOperator = "Eval", + Operand = "xyz", + Operator = "Equal", + }, + new ValueConditionNodeViewModel + { + Condition = "Condition2", + DataType = "Integer", + LogicalOperator = "Eval", + Operand = "123", + Operator = "Equal", + }, + }, + LogicalOperator = "Or", + }, + Ruleset = "Ruleset1", + }; + + // Act + var actual = ruleViewModel.ToExportRulesModel(); + + // Assert + actual.Should().NotBeNull(); + actual.Active.Should().BeTrue(); + actual.Content.Should().NotBeNull() + .And.BeSameAs(ruleViewModel.Content); + actual.DateBegin.Should().Be(ruleViewModel.DateBegin); + actual.DateEnd.Should().Be(ruleViewModel.DateEnd); + actual.Name.Should().Be(ruleViewModel.Name); + actual.Priority.Should().Be(ruleViewModel.Priority); + actual.RootCondition.Should().BeEquivalentTo(ruleViewModel.RootCondition); + actual.Ruleset.Should().Be(ruleViewModel.Ruleset); + } + + [Fact] + public void ToViewModel_GivenRule_ReturnsRuleViewModel() + { + // Arrange + var rule = Rule.Create("Sample name") + .InRuleset("Ruleset1") + .SetContent(new object()) + .Since(DateTime.Parse("2024-10-01Z")) + .Until(DateTime.Parse("2024-10-31Z")) + .ApplyWhen(b => b + .Or(or => or + .Value("Condition1", Operators.Equal, "xyz") + .Value("Condition2", Operators.Equal, 123) + ) + ) + .Build() + .Rule; + rule.Priority = 1; + var expected = new RuleViewModel + { + Active = true, + Content = new object(), + DateBegin = DateTime.Parse("2024-10-01Z"), + DateEnd = DateTime.Parse("2024-10-31Z"), + Id = Guid.Parse("7247859b-3519-f813-cd7b-2c23723673ae"), + Name = "Sample name", + Priority = 1, + RootCondition = new ComposedConditionNodeViewModel + { + ChildConditionNodes = new[] + { + new ValueConditionNodeViewModel + { + Condition = "Condition1", + DataType = "String", + LogicalOperator = "Eval", + Operand = "xyz", + Operator = "Equal", + }, + new ValueConditionNodeViewModel + { + Condition = "Condition2", + DataType = "Integer", + LogicalOperator = "Eval", + Operand = "123", + Operator = "Equal", + }, + }, + LogicalOperator = "Or", + }, + Ruleset = "Ruleset1", + }; + + // Act + var actual = rule.ToViewModel(); + + // Assert + actual.Should().NotBeNull() + .And.BeEquivalentTo(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/WebUIMiddlewareTests.cs b/tests/Rules.Framework.WebUI.Tests/WebUIMiddlewareTests.cs deleted file mode 100644 index cee48598..00000000 --- a/tests/Rules.Framework.WebUI.Tests/WebUIMiddlewareTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Rules.Framework.WebUI.Tests -{ - using System.Collections.Generic; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Logging; - using Moq; - using Rules.Framework.WebUI.Tests.Utilities; - using Xunit; - - public class WebUIMiddlewareTests - { - [Fact] - public async Task InvokeAsync_CallsHttpRequestHandler() - { - var mockHttpRequestHandler = new Mock(); - var mockLoggerFactory = new Mock(); - var mockLogger = new Mock(); - var mockWebHostEnvironment = new Mock(); - - mockLoggerFactory.Setup(d => d.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - var middleware = WebUIMiddlewareFactory.Create(mockWebHostEnvironment.Object, - mockLoggerFactory.Object, - new List { mockHttpRequestHandler.Object }); - - var context = new DefaultHttpContext(); - - // act - await middleware.InvokeAsync(context); - - // assert - mockHttpRequestHandler.Verify(mock => mock.HandleAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/WebUIOptionsRegistryTests.cs b/tests/Rules.Framework.WebUI.Tests/WebUIOptionsRegistryTests.cs new file mode 100644 index 00000000..f39f6e2f --- /dev/null +++ b/tests/Rules.Framework.WebUI.Tests/WebUIOptionsRegistryTests.cs @@ -0,0 +1,41 @@ +namespace Rules.Framework.WebUI.Tests +{ + using System; + using FluentAssertions; + using Xunit; + + public class WebUIOptionsRegistryTests + { + [Fact] + public void Register_GivenNullOptions_ThrowsArgumentNullException() + { + // Arrange + var webUIOptionsRegistry = new WebUIOptionsRegistry(); + + // Act + var exception = Assert.Throws(() => webUIOptionsRegistry.Register(null)); + + // Assert + exception.ParamName.Should().Be("webUIOptions"); + webUIOptionsRegistry.RegisteredOptions.Should().BeNull(); + } + + [Fact] + public void Register_GivenValidOptions_RegistersOptions() + { + // Arrange + var webUIOptions = new WebUIOptions + { + DocumentTitle = "Title", + }; + var webUIOptionsRegistry = new WebUIOptionsRegistry(); + + // Act + webUIOptionsRegistry.Register(webUIOptions); + + // Assert + webUIOptionsRegistry.RegisteredOptions.Should().NotBeNull() + .And.BeSameAs(webUIOptions); + } + } +} \ No newline at end of file From f42d40da3bef20a7bde577fb9184753555db6929 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Fri, 18 Oct 2024 00:30:06 +0100 Subject: [PATCH 02/20] refactor!: revamp Web UI --- .../Components/Layout/MainLayout.razor.css | 0 .../WebUIOtionsRegistryTests.cs | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/Rules.Framework.WebUI/Components/Layout/MainLayout.razor.css create mode 100644 tests/Rules.Framework.WebUI.Tests/WebUIOtionsRegistryTests.cs diff --git a/src/Rules.Framework.WebUI/Components/Layout/MainLayout.razor.css b/src/Rules.Framework.WebUI/Components/Layout/MainLayout.razor.css new file mode 100644 index 00000000..e69de29b diff --git a/tests/Rules.Framework.WebUI.Tests/WebUIOtionsRegistryTests.cs b/tests/Rules.Framework.WebUI.Tests/WebUIOtionsRegistryTests.cs new file mode 100644 index 00000000..25c62569 --- /dev/null +++ b/tests/Rules.Framework.WebUI.Tests/WebUIOtionsRegistryTests.cs @@ -0,0 +1,41 @@ +namespace Rules.Framework.WebUI.Tests +{ + using System; + using FluentAssertions; + using Xunit; + + public class WebUIOtionsRegistryTests + { + [Fact] + public void Register_GivenNullOptions_ThrowsArgumentNullException() + { + // Arrange + var webUIOptionsRegistry = new WebUIOptionsRegistry(); + + // Act + var exception = Assert.Throws(() => webUIOptionsRegistry.Register(null)); + + // Assert + exception.ParamName.Should().Be("webUIOptions"); + webUIOptionsRegistry.RegisteredOptions.Should().BeNull(); + } + + [Fact] + public void Register_GivenValidOptions_RegistersOptions() + { + // Arrange + var webUIOptions = new WebUIOptions + { + DocumentTitle = "Title", + }; + var webUIOptionsRegistry = new WebUIOptionsRegistry(); + + // Act + webUIOptionsRegistry.Register(webUIOptions); + + // Assert + webUIOptionsRegistry.RegisteredOptions.Should().NotBeNull() + .And.BeSameAs(webUIOptions); + } + } +} \ No newline at end of file From 2f729f62f9c2459a06159b0a338cee0f1d86f23f Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sat, 18 May 2024 15:46:56 +0100 Subject: [PATCH 03/20] 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 + .../AssemblyMetadata.cs | 3 + .../Rules.Framework.BenchmarkTests.csproj | 2 +- .../Scenarios/IScenarioData.cs | 2 +- .../Scenarios/Scenario8/PokerConditions.cs | 2 +- .../Scenarios/Scenario8/PokerRulesets.cs | 2 +- .../Scenario8/Scenario8Data.Flush.cs | 2 +- .../Scenario8/Scenario8Data.FourOfAKind.cs | 2 +- .../Scenario8/Scenario8Data.HighCard.cs | 2 +- .../Scenarios/Scenario8/Scenario8Data.Pair.cs | 2 +- .../Scenario8/Scenario8Data.RoyalFlush.cs | 2 +- .../Scenario8/Scenario8Data.Straight.cs | 2 +- .../Scenario8/Scenario8Data.StraightFlush.cs | 2 +- .../Scenario8/Scenario8Data.ThreeOfAKind.cs | 2 +- .../Scenarios/Scenario8/Scenario8Data.cs | 2 +- ...core.cs => SingleCombinationPokerScore.cs} | 2 +- .../Scenarios/ScenarioLoader.cs | 2 - ...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 +- 193 files changed, 10243 insertions(+), 26 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/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 49bda302..0a37ec5e 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.IntegrationTests.Common/Scenarios/IScenarioData.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/IScenarioData.cs index ef924c41..67e398f0 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/Scenario8/PokerConditions.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/PokerConditions.cs index ed6e7611..9be3b25f 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/PokerConditions.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/PokerConditions.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { public enum PokerConditions { diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/PokerRulesets.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/PokerRulesets.cs index 2a911cd7..1e8e6ae4 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/PokerRulesets.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/PokerRulesets.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { public enum PokerRulesets { 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 9ede6652..d4f70aa1 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System.Collections.Generic; using Rules.Framework; 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 da0b6c5e..88cff558 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System.Collections.Generic; using Rules.Framework; 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 684756f9..ecb4c598 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System.Collections.Generic; using Rules.Framework; 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 ae0b04fd..0f52a703 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System.Collections.Generic; using Rules.Framework; 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 b4ac0195..198d69ad 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.Collections.Generic; using Rules.Framework; 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 d17885e7..d789d486 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.Collections.Generic; using Rules.Framework; 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 159b6b15..19de203d 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System.Collections.Generic; using Rules.Framework; 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 e2405a13..030d2671 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System.Collections.Generic; using Rules.Framework; diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.cs index 354549be..ae3dcaa3 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 ddec42b8..624d1dfd 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/ScenarioLoader.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/ScenarioLoader.cs @@ -1,7 +1,5 @@ namespace Rules.Framework.IntegrationTests.Common.Scenarios { - using Rules.Framework.BenchmarkTests.Tests; - public static class ScenarioLoader { public static async Task LoadScenarioAsync( diff --git a/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs b/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs index b2f448d1..ed41707b 100644 --- a/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs +++ b/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs @@ -4,8 +4,8 @@ namespace Rules.Framework.IntegrationTests.Scenarios.Scenario8 using System.Collections.Generic; 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 f26f2c8801c94f5b71b60cabda58ac6f2285982a Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 15:35:34 +0100 Subject: [PATCH 04/20] feat: add RQL support to Web UI --- .../Pipeline/Interpret/Interpreter.cs | 62 ++++++++++++------- ...Extensions.cs => RulesEngineExtensions.cs} | 18 ++++-- .../Rules.Framework.WebUI.csproj | 3 +- .../InterpreterTests.InputConditionSegment.cs | 3 +- .../RulesEngineExtensionsTests.cs | 23 +++++-- ...ensionsTests.cs => RuleExtensionsTests.cs} | 2 +- 6 files changed, 76 insertions(+), 35 deletions(-) rename src/Rules.Framework.Rql/{RuleEngineExtensions.cs => RulesEngineExtensions.cs} (60%) rename tests/Rules.Framework.Tests/Extensions/{GenericRuleExtensionsTests.cs => RuleExtensionsTests.cs} (99%) 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/Rules.Framework.WebUI.csproj b/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj index 572ea993..1d2a6857 100644 --- a/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj +++ b/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj @@ -36,7 +36,8 @@ - + + 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 99% rename from tests/Rules.Framework.Tests/Extensions/GenericRuleExtensionsTests.cs rename to tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs index d17d26db..a66b7cc8 100644 --- a/tests/Rules.Framework.Tests/Extensions/GenericRuleExtensionsTests.cs +++ b/tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs @@ -9,7 +9,7 @@ namespace Rules.Framework.Tests.Extensions using Rules.Framework.Tests.Stubs; using Xunit; - public class GenericRuleExtensionsTests + public class RuleExtensionsTests { [Fact] public void GenericRuleExtensions_ToGenericRule_WithComposedCondition_Success() From f3c5eebe73e85f6a9883d9127087f05c6d354852 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 15:52:52 +0100 Subject: [PATCH 05/20] 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 3b36043992fed52b05ff4f37c820afef23ba7162 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 22:10:06 +0100 Subject: [PATCH 06/20] 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 d0cbb7c6accdd376962f8edb66cadf8e6cca9483 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 23:35:19 +0100 Subject: [PATCH 07/20] 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 +++- .../RqlEngineTests.cs | 8 +++---- .../Runtime/RqlRuntimeTests.cs | 1 - .../Rules.Framework.RqlReplTester/Program.cs | 2 +- 24 files changed, 113 insertions(+), 59 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/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 6b213965930ab059b1fd743b40110677dafa1df8 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Wed, 5 Jun 2024 22:48:11 +0100 Subject: [PATCH 08/20] 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 77760ca881fc58fb411a98bd29fec19aa68fc6c7 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Wed, 5 Jun 2024 23:02:49 +0100 Subject: [PATCH 09/20] 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 6bf8d83795a3cad59b8a8939355ee03079cbf1ad Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Wed, 5 Jun 2024 23:07:53 +0100 Subject: [PATCH 10/20] 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 d379d975f9355b80d434c03e1e0a9adea89dbf39 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sat, 22 Jun 2024 17:52:51 +0100 Subject: [PATCH 11/20] 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)); From 7636fe05e1af04c6c5b5ae51a7310cc5478bfef6 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sat, 2 Nov 2024 17:39:42 +0000 Subject: [PATCH 12/20] chore: adjustments after rebase --- .../Engine/RuleSpecificationBase.cs | 6 +- .../MongoDbProviderRulesDataSource.cs | 16 +-- .../MongoDbProviderSettings.cs | 2 +- ...oStrongTypeContentSerializationProvider.cs | 8 +- .../DynamicToStrongTypeContentSerializer.cs | 12 +- .../Ast/Expressions/MatchExpression.cs | 14 +- .../Ast/Expressions/SearchExpression.cs | 10 +- .../Pipeline/Interpret/Interpreter.cs | 94 +++++-------- .../Strategies/MatchRulesParseStrategy.cs | 4 +- ...rseStrategy.cs => RulesetParseStrategy.cs} | 20 +-- .../Strategies/SearchRulesParseStrategy.cs | 4 +- src/Rules.Framework.Rql/ReverseRqlBuilder.cs | 12 +- src/Rules.Framework.Rql/RqlEngine.cs | 16 +-- src/Rules.Framework.Rql/RqlEngineBuilder.cs | 19 ++- .../RulesEngineExtensions.cs | 16 +-- src/Rules.Framework.Rql/RulesSetResult.cs | 6 +- src/Rules.Framework.Rql/RulesSetResultLine.cs | 6 +- src/Rules.Framework.Rql/Runtime/IRuntime.cs | 10 +- .../Runtime/MatchRulesArgs.cs | 8 +- src/Rules.Framework.Rql/Runtime/RqlRuntime.cs | 44 +++--- .../Runtime/SearchRulesArgs.cs | 8 +- .../Runtime/Types/RqlRule.cs | 24 ++-- .../Runtime/Types/RqlRuleset.cs | 29 ++++ .../Runtime/Types/RqlTypes.cs | 4 + src/Rules.Framework/AssemblyMetadata.cs | 1 + .../Builder/Generic/RuleBuilderResult.cs | 14 +- .../Generic/RulesBuilder/RuleBuilder.cs | 4 +- .../FluentConditionNodeBuilder.cs | 4 +- .../GenericConditionNodeValidationArgs.cs | 6 +- .../Validation/GenericRuleValidator.cs | 18 +-- .../Core/PropertiesDictionary.cs | 3 +- .../ConditionsValueLookupExtension.cs | 4 +- .../ConditionNodes/ValueConditionNode.cs | 2 +- src/Rules.Framework/IRulesDataSource.cs | 10 +- .../InMemory/IInMemoryRulesStorage.cs | 2 +- .../InMemory/InMemoryRulesStorage.cs | 22 +-- src/Rules.Framework/RulesEngine.cs | 16 +-- .../IContentSerializationProvider.cs | 11 +- .../SerializedContentContainer.cs | 6 +- src/Rules.Framework/Source/GetRulesArgs.cs | 4 +- src/Rules.Framework/Source/RulesSource.cs | 2 +- .../Validation/SearchArgsValidator.cs | 16 +-- .../Tests/Benchmark3/Benchmark3.cs | 1 + .../Features/Stubs/ConditionClass.cs | 9 ++ .../Features/Stubs/ConditionTypeClass.cs | 9 -- .../Features/Stubs/ContentTypeClass.cs | 9 -- .../{EmptyContentType.cs => EmptyRuleset.cs} | 2 +- .../Features/Stubs/RulesetClass.cs | 9 ++ .../Scenarios/Scenario6/Scenario6Data.cs | 2 +- .../Scenarios/Scenario7/Scenario7Data.cs | 1 + .../Scenario8/Scenario8Data.Flush.cs | 8 +- .../Scenario8/Scenario8Data.FourOfAKind.cs | 26 ++-- .../Scenario8/Scenario8Data.HighCard.cs | 26 ++-- .../Scenarios/Scenario8/Scenario8Data.Pair.cs | 26 ++-- .../Scenario8/Scenario8Data.RoyalFlush.cs | 8 +- .../Scenario8/Scenario8Data.Straight.cs | 16 +-- .../Scenario8/Scenario8Data.StraightFlush.cs | 64 ++++----- .../Scenario8/Scenario8Data.ThreeOfAKind.cs | 26 ++-- .../JsonContentSerializationProvider.cs | 2 +- .../RulesFromJsonFile.cs | 12 +- .../OperatorContainsManyToOneTests.cs | 12 +- .../Scenario2/CarInsuranceAdvisorTests.cs | 4 +- .../BuildingSecuritySystemControlTests.cs | 6 +- .../MongoDbProviderRulesDataSourceTests.cs | 110 +++++++-------- .../RuleFactoryTests.cs | 2 +- ...ngTypeContentSerializationProviderTests.cs | 2 +- ...namicToStrongTypeContentSerializerTests.cs | 2 +- .../CheckFiles/MatchExpressionChecks.yaml | 48 +++---- .../CheckFiles/SearchExpressionChecks.yaml | 54 +++---- .../RulesEngineWithScenario8RulesFixture.cs | 4 +- ...TexasHoldEmPokerSingleCombinationsTests.cs | 17 ++- .../InterpreterTests.BinaryExpression.cs | 9 +- .../InterpreterTests.CardinalitySegment.cs | 5 +- .../InterpreterTests.ExpressionStatement.cs | 5 +- .../InterpreterTests.IdentifierExpression.cs | 5 +- .../InterpreterTests.InputConditionSegment.cs | 46 ++---- ...InterpreterTests.InputConditionsSegment.cs | 17 ++- .../InterpreterTests.KeywordExpression.cs | 5 +- .../InterpreterTests.LiteralExpression.cs | 9 +- .../InterpreterTests.MatchExpression.cs | 91 +++++++----- .../InterpreterTests.NewArrayExpression.cs | 9 +- .../InterpreterTests.NewObjectExpression.cs | 5 +- .../Interpret/InterpreterTests.None.cs | 13 +- .../InterpreterTests.OperatorSegment.cs | 13 +- .../InterpreterTests.PlaceholderExpression.cs | 9 +- .../InterpreterTests.SearchExpression.cs | 91 +++++++----- .../InterpreterTests.UnaryExpression.cs | 9 +- .../Pipeline/Interpret/InterpreterTests.cs | 11 +- .../ReverseRqlBuilderTests.cs | 34 ++--- .../RqlEngineBuilderTests.cs | 11 +- .../RqlEngineTests.cs | 13 +- .../RulesEngineExtensionsTests.cs | 44 +----- .../Runtime/RqlRuntimeTests.cs | 133 +++++++++--------- .../{ConditionType.cs => Conditions.cs} | 2 +- .../TestStubs/{ContentType.cs => Rulesets.cs} | 2 +- .../Rules.Framework.RqlReplTester/Program.cs | 10 +- .../Builder/RuleBuilderTests.cs | 14 +- .../ConditionNodes/ValueConditionNodeTests.cs | 56 ++++---- .../CompilationRulesSourceMiddlewareTests.cs | 6 +- .../ConditionsValueLookupExtensionTests.cs | 12 +- .../Extensions/RuleExtensionsTests.cs | 4 +- .../Generic/RulesEngineTests.cs | 10 +- .../Providers/InMemory/RuleFactoryTests.cs | 4 +- .../InMemory/TestStubs/RulesetNames.cs | 2 +- .../RuleConditionsExtractorTests.cs | 30 ++-- .../Rules.Framework.Tests/RulesEngineTests.cs | 58 ++++---- .../SerializedContentContainerTests.cs | 6 +- .../Source/RulesSourceTests.cs | 26 ++-- .../Source/StubRulesSourceMiddleware.cs | 4 +- .../Validation/SearchArgsValidatorTests.cs | 46 +++--- 110 files changed, 943 insertions(+), 990 deletions(-) rename src/Rules.Framework.Rql/Pipeline/Parse/Strategies/{ContentTypeParseStrategy.cs => RulesetParseStrategy.cs} (50%) create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs create mode 100644 tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionClass.cs delete mode 100644 tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionTypeClass.cs delete mode 100644 tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ContentTypeClass.cs rename tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/{EmptyContentType.cs => EmptyRuleset.cs} (61%) create mode 100644 tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/RulesetClass.cs rename tests/Rules.Framework.Rql.Tests/TestStubs/{ConditionType.cs => Conditions.cs} (85%) rename tests/Rules.Framework.Rql.Tests/TestStubs/{ContentType.cs => Rulesets.cs} (76%) diff --git a/samples/Rules.Framework.WebUI.Sample/Engine/RuleSpecificationBase.cs b/samples/Rules.Framework.WebUI.Sample/Engine/RuleSpecificationBase.cs index b0b408e5..e41d3830 100644 --- a/samples/Rules.Framework.WebUI.Sample/Engine/RuleSpecificationBase.cs +++ b/samples/Rules.Framework.WebUI.Sample/Engine/RuleSpecificationBase.cs @@ -3,10 +3,10 @@ namespace Rules.Framework.WebUI.Sample.Engine using global::Rules.Framework; using global::Rules.Framework.Builder.Generic; - internal class RuleSpecificationBase + internal class RuleSpecificationBase { public RuleSpecificationBase( - RuleBuilderResult ruleBuilderResult, + RuleBuilderResult ruleBuilderResult, RuleAddPriorityOption ruleAddPriorityOption) { this.RuleBuilderResult = ruleBuilderResult; @@ -15,6 +15,6 @@ public RuleSpecificationBase( public RuleAddPriorityOption RuleAddPriorityOption { get; set; } - public RuleBuilderResult RuleBuilderResult { get; set; } + public RuleBuilderResult RuleBuilderResult { get; set; } } } \ No newline at end of file diff --git a/src/Rules.Framework.Providers.MongoDb/MongoDbProviderRulesDataSource.cs b/src/Rules.Framework.Providers.MongoDb/MongoDbProviderRulesDataSource.cs index a69eb0c3..83628fb7 100644 --- a/src/Rules.Framework.Providers.MongoDb/MongoDbProviderRulesDataSource.cs +++ b/src/Rules.Framework.Providers.MongoDb/MongoDbProviderRulesDataSource.cs @@ -48,8 +48,8 @@ public async Task AddRuleAsync(Rule rule) /// /// Creates a new ruleset on the data source. /// - /// Type of the content. - public async Task CreateRulesetAsync(string contentType) + /// The ruleset name. + public async Task CreateRulesetAsync(string ruleset) { var rulesetsCollection = this.mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesetsCollectionName); @@ -57,7 +57,7 @@ public async Task CreateRulesetAsync(string contentType) { Creation = DateTime.UtcNow, Id = Guid.NewGuid(), - Name = contentType, + Name = ruleset, }; await rulesetsCollection.InsertOneAsync(rulesetDataModel).ConfigureAwait(false); @@ -74,7 +74,7 @@ public async Task CreateRulesetAsync(string contentType) public async Task> GetRulesAsync(string ruleset, DateTime dateBegin, DateTime dateEnd) { var getRulesByRulesetAndDatesInterval = MongoDbProviderRulesDataSource - .BuildFilterByContentTypeAndDatesInterval(ruleset, dateBegin, dateEnd); + .BuildFilterByRulesetAndDatesInterval(ruleset, dateBegin, dateEnd); return await this.GetRulesAsync(getRulesByRulesetAndDatesInterval).ConfigureAwait(false); } @@ -98,7 +98,7 @@ public Task> GetRulesByAsync(RulesFilterArgs rulesFilterArgs) } /// - /// Gets the content types from the data source. + /// Gets the rulesets from the data source. /// /// public async Task> GetRulesetsAsync() @@ -149,7 +149,7 @@ public async Task UpdateRuleAsync(Rule rule) await rulesCollection.UpdateOneAsync(filterDefinition, updateDefinition).ConfigureAwait(false); } - private static FilterDefinition BuildFilterByContentTypeAndDatesInterval(string ruleset, DateTime dateBegin, DateTime dateEnd) + private static FilterDefinition BuildFilterByRulesetAndDatesInterval(string ruleset, DateTime dateBegin, DateTime dateEnd) { var rulesetFilter = Builders.Filter.Eq(x => x.Ruleset, ruleset); @@ -185,11 +185,11 @@ private static FilterDefinition BuildFilterFromRulesFilterArgs(Ru return filtersToApply.Any() ? Builders.Filter.And(filtersToApply) : Builders.Filter.Empty; } - private async Task> GetRulesAsync(FilterDefinition getRulesByContentTypeAndDatesInterval) + private async Task> GetRulesAsync(FilterDefinition getRulesByRulesetAndDatesInterval) { var rulesCollection = this.mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesCollectionName); - var fetchedRulesCursor = await rulesCollection.FindAsync(getRulesByContentTypeAndDatesInterval).ConfigureAwait(false); + var fetchedRulesCursor = await rulesCollection.FindAsync(getRulesByRulesetAndDatesInterval).ConfigureAwait(false); var fetchedRules = await fetchedRulesCursor.ToListAsync().ConfigureAwait(false); diff --git a/src/Rules.Framework.Providers.MongoDb/MongoDbProviderSettings.cs b/src/Rules.Framework.Providers.MongoDb/MongoDbProviderSettings.cs index 1a0be271..2edd3435 100644 --- a/src/Rules.Framework.Providers.MongoDb/MongoDbProviderSettings.cs +++ b/src/Rules.Framework.Providers.MongoDb/MongoDbProviderSettings.cs @@ -30,7 +30,7 @@ public MongoDbProviderSettings() /// /// Gets or sets the name of the rulesets collection. /// - /// The name of the content types collection. + /// The name of the rulesets collection. public string RulesetsCollectionName { get; set; } } } \ No newline at end of file diff --git a/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializationProvider.cs b/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializationProvider.cs index 42a79da4..4c4b5152 100644 --- a/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializationProvider.cs +++ b/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializationProvider.cs @@ -22,11 +22,7 @@ public DynamicToStrongTypeContentSerializationProvider() System.Threading.LazyThreadSafetyMode.PublicationOnly); } - /// - /// Gets the content serializer associated with the given . - /// - /// the content type. - /// the content serializer to deal with contents for specified content type. - public IContentSerializer GetContentSerializer(string contentType) => this.contentSerializerLazy.Value; + /// + public IContentSerializer GetContentSerializer(string ruleset) => this.contentSerializerLazy.Value; } } \ No newline at end of file diff --git a/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializer.cs b/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializer.cs index 343c17e4..950f8eb6 100644 --- a/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializer.cs +++ b/src/Rules.Framework.Providers.MongoDb/Serialization/DynamicToStrongTypeContentSerializer.cs @@ -33,15 +33,15 @@ public object Deserialize(object serializedContent, Type type) throw new ArgumentNullException(nameof(type)); } - var serializedContentType = serializedContent.GetType(); - if (serializedContentType.IsValueType || stringType.IsAssignableFrom(serializedContentType)) + var serializedRuleset = serializedContent.GetType(); + if (serializedRuleset.IsValueType || stringType.IsAssignableFrom(serializedRuleset)) { return Parse(serializedContent, type); } - if (!expandoObjectType.IsAssignableFrom(serializedContentType)) + if (!expandoObjectType.IsAssignableFrom(serializedRuleset)) { - throw new NotSupportedException($"The serialized content type is not supported for deserialization: {serializedContent.GetType().FullName}"); + throw new NotSupportedException($"The serialized ruleset is not supported for deserialization: {serializedContent.GetType().FullName}"); } if (type == objectType || type == expandoObjectType) @@ -98,9 +98,9 @@ private static object DeserializeToType(IDictionary serializedCo throw new NotSupportedException($"The target type '{type.FullName}' must define a default (no parameters) constructor.", mme); } - foreach (string key in serializedContentDictionary.Keys) + foreach (var key in serializedContentDictionary.Keys) { - if (reflectedProperties.TryGetValue(key, out PropertyInfo currentPropertyInfo)) + if (reflectedProperties.TryGetValue(key, out var currentPropertyInfo)) { var serializedPropertyValue = serializedContentDictionary[key]; diff --git a/src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs index 76171790..5ee98128 100644 --- a/src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs +++ b/src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs @@ -8,30 +8,30 @@ internal class MatchExpression : Expression { private MatchExpression( Segment cardinality, - Expression contentType, + Expression ruleset, Expression matchDate, Segment inputConditions) : base(cardinality.BeginPosition, inputConditions?.EndPosition ?? matchDate.EndPosition) { this.Cardinality = cardinality; - this.ContentType = contentType; - this.MatchDate = matchDate; this.InputConditions = inputConditions; + this.MatchDate = matchDate; + this.Ruleset = ruleset; } public Segment Cardinality { get; } - public Expression ContentType { get; } - public Segment InputConditions { get; } public Expression MatchDate { get; } + public Expression Ruleset { get; } + public static MatchExpression Create(Segment cardinality, - Expression contentType, + Expression ruleset, Expression matchDate, Segment inputConditions) - => new(cardinality, contentType, matchDate, inputConditions); + => new(cardinality, ruleset, matchDate, inputConditions); public override T Accept(IExpressionVisitor visitor) => visitor.VisitMatchExpression(this); } diff --git a/src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs index 84926017..b5b80ff0 100644 --- a/src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs +++ b/src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs @@ -6,26 +6,26 @@ namespace Rules.Framework.Rql.Ast.Expressions [ExcludeFromCodeCoverage] internal class SearchExpression : Expression { - public SearchExpression(Expression contentType, + public SearchExpression(Expression ruleset, Expression dateBegin, Expression dateEnd, Segment inputConditions) - : base(contentType.BeginPosition, inputConditions?.EndPosition ?? dateEnd.EndPosition) + : base(ruleset.BeginPosition, inputConditions?.EndPosition ?? dateEnd.EndPosition) { - this.ContentType = contentType; this.DateBegin = dateBegin; this.DateEnd = dateEnd; this.InputConditions = inputConditions; + this.Ruleset = ruleset; } - public Expression ContentType { get; } - public Expression DateBegin { get; } public Expression DateEnd { get; } public Segment InputConditions { get; } + public Expression Ruleset { get; } + public override T Accept(IExpressionVisitor visitor) => visitor.VisitSearchExpression(this); } } \ 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 index a26762f4..e9e3f2db 100644 --- a/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs @@ -13,14 +13,14 @@ namespace Rules.Framework.Rql.Pipeline.Interpret using Rules.Framework.Rql.Runtime.Types; using Rules.Framework.Rql.Tokens; - internal class Interpreter : IInterpreter, IExpressionVisitor>, ISegmentVisitor>, IStatementVisitor> + internal class Interpreter : IInterpreter, IExpressionVisitor>, ISegmentVisitor>, IStatementVisitor> { private readonly IReverseRqlBuilder reverseRqlBuilder; - private readonly IRuntime runtime; + private readonly IRuntime runtime; private bool disposedValue; public Interpreter( - IRuntime runtime, + IRuntime runtime, IReverseRqlBuilder reverseRqlBuilder) { this.runtime = runtime; @@ -82,19 +82,20 @@ public Task VisitIdentifierExpression(IdentifierExpression identi public async Task VisitInputConditionSegment(InputConditionSegment inputConditionExpression) { - var conditionType = await this.HandleConditionTypeAsync(inputConditionExpression.Left).ConfigureAwait(false); + var conditionName = await this.HandleConditionNameAsync(inputConditionExpression.Left).ConfigureAwait(false); var conditionValue = await inputConditionExpression.Right.Accept(this).ConfigureAwait(false); - return new Condition(conditionType, conditionValue.RuntimeValue); + return (conditionName, 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++) + var conditions = new Dictionary(StringComparer.Ordinal); + for (var i = 0; i < inputConditionsLength; i++) { - conditions[i] = (Condition)await inputConditions[i].Accept(this).ConfigureAwait(false); + (var conditionName, var conditionValue) = (ValueTuple)await inputConditions[i].Accept(this).ConfigureAwait(false); + conditions[conditionName] = conditionValue; } return conditions; @@ -128,19 +129,21 @@ public async Task VisitMatchExpression(MatchExpression matchExpre try { var cardinality = (RqlString)await matchExpression.Cardinality.Accept(this).ConfigureAwait(false); - var contentType = await this.HandleContentTypeAsync(matchExpression.ContentType).ConfigureAwait(false); + var ruleset = await this.HandleRulesetAsync(matchExpression.Ruleset).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 conditions = inputConditions is null + ? new Dictionary(StringComparer.Ordinal) + : (IDictionary)inputConditions; var matchCardinality = string.Equals(cardinality.Value, "ONE", StringComparison.OrdinalIgnoreCase) ? MatchCardinality.One : MatchCardinality.All; - var matchRulesArgs = new MatchRulesArgs + var matchRulesArgs = new MatchRulesArgs { Conditions = conditions, - ContentType = contentType, MatchCardinality = matchCardinality, MatchDate = matchDate, + Ruleset = ruleset, }; return await this.runtime.MatchRulesAsync(matchRulesArgs).ConfigureAwait(false); @@ -174,7 +177,7 @@ public async Task VisitNewObjectExpression(NewObjectExpression ne { var rqlObject = new RqlObject(); var propertyAssignments = newObjectExpression.PropertyAssignments; - for (int i = 0; i < propertyAssignments.Length; i++) + for (var i = 0; i < propertyAssignments.Length; i++) { var assignment = (AssignmentExpression)propertyAssignments[i]; var left = (RqlString)await assignment.Left.Accept(this).ConfigureAwait(false); @@ -285,14 +288,14 @@ public async Task VisitSearchExpression(SearchExpression searchEx { try { - var contentType = await this.HandleContentTypeAsync(searchExpression.ContentType).ConfigureAwait(false); + var ruleset = await this.HandleRulesetAsync(searchExpression.Ruleset).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 + var conditions = (IDictionary)await searchExpression.InputConditions.Accept(this).ConfigureAwait(false); + var searchRulesArgs = new SearchRulesArgs { - Conditions = conditions ?? Enumerable.Empty>(), - ContentType = contentType, + Conditions = conditions ?? new Dictionary(StringComparer.Ordinal), + Ruleset = ruleset, DateBegin = dateBegin, DateEnd = dateEnd, }; @@ -336,61 +339,28 @@ private Exception CreateInterpreterException(string error, IAstElement astElemen return CreateInterpreterException(new[] { error }, astElement); } - private async Task HandleConditionTypeAsync(Expression conditionTypeExpression) + private async Task HandleConditionNameAsync(Expression conditionExpression) { - 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; + var conditionName = (RqlString)await conditionExpression.Accept(this).ConfigureAwait(false); + return conditionName.Value; } - private async Task HandleContentTypeAsync(Expression contentTypeExpression) + private async Task HandleRulesetAsync(Expression rulesetExpression) { - var rawValue = await contentTypeExpression.Accept(this).ConfigureAwait(false); + var rawValue = await rulesetExpression.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); + throw CreateInterpreterException($"Expected a ruleset value of type '{RqlTypes.String.Name}' but found '{value.Type.Name}' instead", rulesetExpression); } - try - { - 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) + var rulesets = await this.runtime.GetRulesetsAsync(); + if (!rulesets.Value.Select(r => r.Unwrap().Value.Name).Contains(value.RuntimeValue)) { - throw CreateInterpreterException($"The content type value '{value.RuntimeValue}' was not found", contentTypeExpression); + throw CreateInterpreterException($"The ruleset '{value.RuntimeValue}' was not found", rulesetExpression); } + + return ((RqlString)value).Value; } } } \ 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 index b39eea11..2ca97d38 100644 --- a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/MatchRulesParseStrategy.cs +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/MatchRulesParseStrategy.cs @@ -32,7 +32,7 @@ public override Expression Parse(ParseContext parseContext) return Expression.None; } - var contentType = this.ParseExpressionWith(parseContext); + var ruleset = this.ParseExpressionWith(parseContext); if (parseContext.PanicMode) { return Expression.None; @@ -65,7 +65,7 @@ public override Expression Parse(ParseContext parseContext) inputConditionsExpression = Segment.None; } - return MatchExpression.Create(cardinality, contentType, matchDate, inputConditionsExpression); + return MatchExpression.Create(cardinality, ruleset, matchDate, inputConditionsExpression); } private Expression ParseDate(ParseContext parseContext) diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ContentTypeParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/RulesetParseStrategy.cs similarity index 50% rename from src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ContentTypeParseStrategy.cs rename to src/Rules.Framework.Rql/Pipeline/Parse/Strategies/RulesetParseStrategy.cs index fbe3baa3..3bf99a25 100644 --- a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ContentTypeParseStrategy.cs +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/RulesetParseStrategy.cs @@ -5,14 +5,14 @@ namespace Rules.Framework.Rql.Pipeline.Parse.Strategies using Rules.Framework.Rql.Ast.Expressions; using Rules.Framework.Rql.Tokens; - internal class ContentTypeParseStrategy : ParseStrategyBase, IExpressionParseStrategy + internal class RulesetParseStrategy : ParseStrategyBase, IExpressionParseStrategy { - private static readonly LiteralType[] allowedLiteralTypesAsContentType = new[] { LiteralType.Integer, LiteralType.String }; + private static readonly LiteralType[] allowedLiteralTypesAsRuleset = 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."); + $"Only literals of types [{allowedLiteralTypesAsRuleset.Select(t => t.ToString()).Aggregate((t1, t2) => $"{t1}, {t2}")}] are allowed."); - public ContentTypeParseStrategy(IParseStrategyProvider parseStrategyProvider) : base(parseStrategyProvider) + public RulesetParseStrategy(IParseStrategyProvider parseStrategyProvider) : base(parseStrategyProvider) { } @@ -20,28 +20,28 @@ public override Expression Parse(ParseContext parseContext) { if (!parseContext.IsMatchCurrentToken(TokenType.FOR)) { - throw new InvalidOperationException("Unable to handle content type expression."); + throw new InvalidOperationException("Unable to handle ruleset expression."); } if (!parseContext.MoveNext()) { - parseContext.EnterPanicMode("Expected content type name.", parseContext.GetNextToken()); + parseContext.EnterPanicMode("Expected ruleset name.", parseContext.GetNextToken()); return Expression.None; } - var contentExpression = this.ParseExpressionWith(parseContext); + var rulesetNameExpression = this.ParseExpressionWith(parseContext); if (parseContext.PanicMode) { return Expression.None; } - if (contentExpression is LiteralExpression literalExpression && !allowedLiteralTypesAsContentType.Contains(literalExpression.Type)) + if (rulesetNameExpression is LiteralExpression literalExpression && !allowedLiteralTypesAsRuleset.Contains(literalExpression.Type)) { - parseContext.EnterPanicMode($"Literal '{literalExpression.Token.Lexeme}' is not allowed as a valid content type. {allowedLiteralTypesMessage.Value}", literalExpression.Token); + parseContext.EnterPanicMode($"Literal '{literalExpression.Token.Lexeme}' is not allowed as a valid ruleset. {allowedLiteralTypesMessage.Value}", literalExpression.Token); return Expression.None; } - return contentExpression; + return rulesetNameExpression; } } } \ 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 index 47e551d6..81134fd9 100644 --- a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/SearchRulesParseStrategy.cs +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/SearchRulesParseStrategy.cs @@ -31,7 +31,7 @@ public override Expression Parse(ParseContext parseContext) return Expression.None; } - var contentType = this.ParseExpressionWith(parseContext); + var ruleset = this.ParseExpressionWith(parseContext); if (parseContext.PanicMode) { return Expression.None; @@ -106,7 +106,7 @@ public override Expression Parse(ParseContext parseContext) inputConditionsExpression = Segment.None; } - return new SearchExpression(contentType, dateBegin, dateEnd, inputConditionsExpression); + return new SearchExpression(ruleset, dateBegin, dateEnd, inputConditionsExpression); } } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/ReverseRqlBuilder.cs b/src/Rules.Framework.Rql/ReverseRqlBuilder.cs index 018545b2..c2b0ac6b 100644 --- a/src/Rules.Framework.Rql/ReverseRqlBuilder.cs +++ b/src/Rules.Framework.Rql/ReverseRqlBuilder.cs @@ -100,7 +100,7 @@ public string VisitInputConditionsSegment(InputConditionsSegment inputConditions public string VisitMatchExpression(MatchExpression matchExpression) { var cardinality = matchExpression.Cardinality.Accept(this); - var contentType = matchExpression.ContentType.Accept(this); + var ruleset = matchExpression.Ruleset.Accept(this); var matchDate = matchExpression.MatchDate.Accept(this); var inputConditions = matchExpression.InputConditions.Accept(this); @@ -110,7 +110,7 @@ public string VisitMatchExpression(MatchExpression matchExpression) .Append(SPACE) .Append("FOR") .Append(SPACE) - .Append(contentType) + .Append(ruleset) .Append(SPACE) .Append("ON") .Append(SPACE) @@ -137,7 +137,7 @@ public string VisitNewArrayExpression(NewArrayExpression newArrayExpression) } else { - for (int i = 0; i < newArrayExpression.Values.Length; i++) + for (var i = 0; i < newArrayExpression.Values.Length; i++) { stringBuilder.Append(SPACE) .Append(newArrayExpression.Values[i].Accept(this)); @@ -163,7 +163,7 @@ public string VisitNewObjectExpression(NewObjectExpression newObjectExpression) { stringBuilder.AppendLine() .Append('{'); - for (int i = 0; i < newObjectExpression.PropertyAssignments.Length; i++) + for (var i = 0; i < newObjectExpression.PropertyAssignments.Length; i++) { var propertyAssignment = newObjectExpression.PropertyAssignments[i].Accept(this); stringBuilder.AppendLine() @@ -202,7 +202,7 @@ public string VisitOperatorSegment(OperatorSegment operatorExpression) public string VisitSearchExpression(SearchExpression searchExpression) { - var contentType = searchExpression.ContentType.Accept(this); + var ruleset = searchExpression.Ruleset.Accept(this); var dateBegin = searchExpression.DateBegin.Accept(this); var dateEnd = searchExpression.DateEnd.Accept(this); var inputConditions = searchExpression.InputConditions.Accept(this); @@ -211,7 +211,7 @@ public string VisitSearchExpression(SearchExpression searchExpression) .Append(SPACE) .Append("FOR") .Append(SPACE) - .Append(contentType) + .Append(ruleset) .Append(SPACE) .Append("SINCE") .Append(SPACE) diff --git a/src/Rules.Framework.Rql/RqlEngine.cs b/src/Rules.Framework.Rql/RqlEngine.cs index 3b65ec66..96585863 100644 --- a/src/Rules.Framework.Rql/RqlEngine.cs +++ b/src/Rules.Framework.Rql/RqlEngine.cs @@ -10,7 +10,7 @@ namespace Rules.Framework.Rql using Rules.Framework.Rql.Pipeline.Scan; using Rules.Framework.Rql.Runtime.Types; - internal class RqlEngine : IRqlEngine + internal class RqlEngine : IRqlEngine { private const string ExceptionMessage = "Errors have occurred processing provided RQL source"; private const string RqlErrorSourceUnavailable = ""; @@ -89,18 +89,18 @@ ExpressionStatementResult expressionStatementResult when IsRulesSetResult(expres _ => throw new NotSupportedException($"Result of type '{result.GetType().FullName}' is not supported."), }; - private static RulesSetResult ConvertToRulesSetResult(ExpressionStatementResult expressionStatementResult) + 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 lines = new List(rqlArray.Size); + for (var i = 0; i < rqlArray.Size; i++) { - var rule = rqlArray.Value[i].Unwrap>(); - var rulesSetResultLine = new RulesSetResultLine(i + 1, rule); + var rule = rqlArray.Value[i].Unwrap(); + var rulesSetResultLine = new RulesSetResultLine(i + 1, rule); lines.Add(rulesSetResultLine); } - return new RulesSetResult(expressionStatementResult.Rql, rqlArray.Size, lines); + return new RulesSetResult(expressionStatementResult.Rql, rqlArray.Size, lines); } private static bool IsRulesSetResult(ExpressionStatementResult expressionStatementResult) @@ -112,7 +112,7 @@ private static bool IsRulesSetResult(ExpressionStatementResult expressionStateme return false; } - for (int i = 0; i < rqlArray.Size; i++) + for (var i = 0; i < rqlArray.Size; i++) { if (rqlArray.Value[i].UnderlyingType != RqlTypes.Rule) { diff --git a/src/Rules.Framework.Rql/RqlEngineBuilder.cs b/src/Rules.Framework.Rql/RqlEngineBuilder.cs index 83053697..9366cbd2 100644 --- a/src/Rules.Framework.Rql/RqlEngineBuilder.cs +++ b/src/Rules.Framework.Rql/RqlEngineBuilder.cs @@ -5,36 +5,35 @@ namespace Rules.Framework.Rql using Rules.Framework.Rql.Pipeline.Parse; using Rules.Framework.Rql.Pipeline.Scan; using Rules.Framework.Rql.Runtime; - using Rules.Framework.Source; - internal class RqlEngineBuilder + internal sealed class RqlEngineBuilder { - private readonly IRulesEngine rulesEngine; + private readonly IRulesEngine rulesEngine; private RqlOptions options; - private RqlEngineBuilder(IRulesEngine rulesEngine) + private RqlEngineBuilder(IRulesEngine rulesEngine) { this.rulesEngine = rulesEngine; } - public static RqlEngineBuilder CreateRqlEngine(IRulesEngine rulesEngine) + public static RqlEngineBuilder CreateRqlEngine(IRulesEngine rulesEngine) { if (rulesEngine is null) { throw new ArgumentNullException(nameof(rulesEngine)); } - return new RqlEngineBuilder(rulesEngine); + return new RqlEngineBuilder(rulesEngine); } public IRqlEngine Build() { - var runtime = RqlRuntime.Create(this.rulesEngine); + var runtime = RqlRuntime.Create(this.rulesEngine); var tokenScanner = new TokenScanner(); var parseStrategyProvider = new ParseStrategyPool(); var parser = new Parser(parseStrategyProvider); var reverseRqlBuilder = new ReverseRqlBuilder(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); var args = new RqlEngineArgs { Interpreter = interpreter, @@ -43,10 +42,10 @@ public IRqlEngine Build() TokenScanner = tokenScanner, }; - return new RqlEngine(args); + return new RqlEngine(args); } - public RqlEngineBuilder WithOptions(RqlOptions options) + public RqlEngineBuilder WithOptions(RqlOptions options) { if (options is null) { diff --git a/src/Rules.Framework.Rql/RulesEngineExtensions.cs b/src/Rules.Framework.Rql/RulesEngineExtensions.cs index 84571f5f..c5dce948 100644 --- a/src/Rules.Framework.Rql/RulesEngineExtensions.cs +++ b/src/Rules.Framework.Rql/RulesEngineExtensions.cs @@ -5,24 +5,14 @@ namespace Rules.Framework public static class RulesEngineExtensions { - public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine) + public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine) { return rulesEngine.GetRqlEngine(RqlOptions.NewWithDefaults()); } - public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine, RqlOptions rqlOptions) + public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine, RqlOptions rqlOptions) { - if (!IsSupportedType(typeof(TContentType))) - { - throw new NotSupportedException($"Rule Query Language is only supported for enum types or strings on {nameof(TContentType)}."); - } - - if (!IsSupportedType(typeof(TConditionType))) - { - throw new NotSupportedException($"Rule Query Language is only supported for enum types or strings on {nameof(TConditionType)}."); - } - - return RqlEngineBuilder.CreateRqlEngine(rulesEngine) + return RqlEngineBuilder.CreateRqlEngine(rulesEngine) .WithOptions(rqlOptions) .Build(); } diff --git a/src/Rules.Framework.Rql/RulesSetResult.cs b/src/Rules.Framework.Rql/RulesSetResult.cs index d9dbdb3f..6103bffb 100644 --- a/src/Rules.Framework.Rql/RulesSetResult.cs +++ b/src/Rules.Framework.Rql/RulesSetResult.cs @@ -4,16 +4,16 @@ namespace Rules.Framework.Rql using System.Diagnostics.CodeAnalysis; [ExcludeFromCodeCoverage] - public class RulesSetResult : IResult + public class RulesSetResult : IResult { - public RulesSetResult(string rql, int numberOfRules, IReadOnlyList> lines) + public RulesSetResult(string rql, int numberOfRules, IReadOnlyList lines) { this.Rql = rql; this.NumberOfRules = numberOfRules; this.Lines = lines; } - public IReadOnlyList> Lines { get; } + public IReadOnlyList Lines { get; } public int NumberOfRules { get; } diff --git a/src/Rules.Framework.Rql/RulesSetResultLine.cs b/src/Rules.Framework.Rql/RulesSetResultLine.cs index 101bab6a..3b0c030a 100644 --- a/src/Rules.Framework.Rql/RulesSetResultLine.cs +++ b/src/Rules.Framework.Rql/RulesSetResultLine.cs @@ -4,9 +4,9 @@ namespace Rules.Framework.Rql using Rules.Framework.Rql.Runtime.Types; [ExcludeFromCodeCoverage] - public class RulesSetResultLine + public class RulesSetResultLine { - internal RulesSetResultLine(int lineNumber, RqlRule rule) + internal RulesSetResultLine(int lineNumber, RqlRule rule) { this.LineNumber = lineNumber; this.Rule = rule; @@ -14,6 +14,6 @@ internal RulesSetResultLine(int lineNumber, RqlRule Rule { get; } + public RqlRule Rule { get; } } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/IRuntime.cs b/src/Rules.Framework.Rql/Runtime/IRuntime.cs index f7f32f7f..d639d189 100644 --- a/src/Rules.Framework.Rql/Runtime/IRuntime.cs +++ b/src/Rules.Framework.Rql/Runtime/IRuntime.cs @@ -1,18 +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 + internal interface IRuntime { IRuntimeValue ApplyBinary(IRuntimeValue leftOperand, RqlOperators rqlOperator, IRuntimeValue rightOperand); IRuntimeValue ApplyUnary(IRuntimeValue value, RqlOperators rqlOperator); - ValueTask MatchRulesAsync(MatchRulesArgs matchRulesArgs); + ValueTask GetRulesetsAsync(); - ValueTask SearchRulesAsync(SearchRulesArgs searchRulesArgs); + ValueTask MatchRulesAsync(MatchRulesArgs matchRulesArgs); + + ValueTask SearchRulesAsync(SearchRulesArgs searchRulesArgs); } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs b/src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs index 60871bcf..0c1cab78 100644 --- a/src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs +++ b/src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs @@ -4,14 +4,14 @@ namespace Rules.Framework.Rql.Runtime using Rules.Framework.Rql.Runtime.RuleManipulation; using Rules.Framework.Rql.Runtime.Types; - internal sealed class MatchRulesArgs + internal sealed class MatchRulesArgs { - public IEnumerable> Conditions { get; set; } - - public TContentType ContentType { get; set; } + public IDictionary Conditions { get; set; } public MatchCardinality MatchCardinality { get; set; } public RqlDate MatchDate { get; set; } + + public string Ruleset { get; set; } } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/RqlRuntime.cs b/src/Rules.Framework.Rql/Runtime/RqlRuntime.cs index 93e01f31..66a3f5c7 100644 --- a/src/Rules.Framework.Rql/Runtime/RqlRuntime.cs +++ b/src/Rules.Framework.Rql/Runtime/RqlRuntime.cs @@ -1,25 +1,24 @@ 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 + internal class RqlRuntime : IRuntime { - private readonly IRulesEngine rulesEngine; + private readonly IRulesEngine rulesEngine; - private RqlRuntime(IRulesEngine rulesEngine) + private RqlRuntime(IRulesEngine rulesEngine) { this.rulesEngine = rulesEngine; } - public static IRuntime Create( - IRulesEngine rulesEngine) + public static IRuntime Create( + IRulesEngine rulesEngine) { - return new RqlRuntime(rulesEngine); + return new RqlRuntime(rulesEngine); } public IRuntimeValue ApplyBinary(IRuntimeValue leftOperand, RqlOperators rqlOperator, IRuntimeValue rightOperand) @@ -64,7 +63,20 @@ public IRuntimeValue ApplyUnary(IRuntimeValue value, RqlOperators rqlOperator) throw new RuntimeException($"Unary operator {rqlOperator} is not supported for value '{value}'."); } - public async ValueTask MatchRulesAsync(MatchRulesArgs matchRulesArgs) + public async ValueTask GetRulesetsAsync() + { + var rulesets = await this.rulesEngine.GetRulesetsAsync(); + var rqlArrayRulesets = new RqlArray(rulesets.Count()); + var i = 0; + foreach (var ruleset in rulesets) + { + rqlArrayRulesets.SetAtIndex(i++, new RqlRuleset(ruleset)); + } + + return rqlArrayRulesets; + } + + public async ValueTask MatchRulesAsync(MatchRulesArgs matchRulesArgs) { if (matchRulesArgs.MatchCardinality == MatchCardinality.None) { @@ -73,32 +85,32 @@ public async ValueTask MatchRulesAsync(MatchRulesArgs(rule)); + 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 rules = await this.rulesEngine.MatchManyAsync(matchRulesArgs.Ruleset, 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)); + rqlArrayAll.SetAtIndex(i++, new RqlRule(rule)); } return rqlArrayAll; } - public async ValueTask SearchRulesAsync(SearchRulesArgs searchRulesArgs) + public async ValueTask SearchRulesAsync(SearchRulesArgs searchRulesArgs) { - var searchArgs = new SearchArgs( - searchRulesArgs.ContentType, + var searchArgs = new SearchArgs( + searchRulesArgs.Ruleset, searchRulesArgs.DateBegin.Value, searchRulesArgs.DateEnd.Value) { @@ -111,7 +123,7 @@ public async ValueTask SearchRulesAsync(SearchRulesArgs(rule)); + rqlArray.SetAtIndex(i++, new RqlRule(rule)); } return rqlArray; diff --git a/src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs b/src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs index fd4eb2b6..25acd356 100644 --- a/src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs +++ b/src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs @@ -3,14 +3,14 @@ namespace Rules.Framework.Rql.Runtime using System.Collections.Generic; using Rules.Framework.Rql.Runtime.Types; - internal sealed class SearchRulesArgs + internal sealed class SearchRulesArgs { - public IEnumerable> Conditions { get; set; } - - public TContentType ContentType { get; set; } + public IDictionary Conditions { get; set; } public RqlDate DateBegin { get; set; } public RqlDate DateEnd { get; set; } + + public string Ruleset { get; set; } } } \ 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 index 83f4d3f3..5241daa2 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs @@ -4,15 +4,14 @@ namespace Rules.Framework.Rql.Runtime.Types using System.Collections.Generic; using System.Linq; using System.Text; - using Rules.Framework.Core; - using Rules.Framework.Core.ConditionNodes; + using Rules.Framework.ConditionNodes; - public readonly struct RqlRule : IRuntimeValue, IEquatable> + public readonly struct RqlRule : IRuntimeValue, IEquatable { - private static readonly Type runtimeType = typeof(Rule); + private static readonly Type runtimeType = typeof(Rule); private readonly Dictionary properties; - internal RqlRule(Rule rule) + internal RqlRule(Rule rule) { this.Value = rule; this.properties = new Dictionary(StringComparer.Ordinal) @@ -23,6 +22,7 @@ internal RqlRule(Rule rule) { "Name", new RqlString(rule.Name) }, { "Priority", new RqlInteger(rule.Priority) }, { "RootCondition", rule.RootCondition is not null ? ConvertCondition(rule.RootCondition) : new RqlNothing() }, + { "Ruleset", new RqlString(rule.Ruleset) }, }; } @@ -32,11 +32,11 @@ internal RqlRule(Rule rule) public RqlType Type => RqlTypes.Rule; - public readonly Rule Value { get; } + public readonly Rule Value { get; } - public static implicit operator RqlAny(RqlRule rqlRule) => new RqlAny(rqlRule); + public static implicit operator RqlAny(RqlRule rqlRule) => new RqlAny(rqlRule); - public bool Equals(RqlRule other) => this.Value.Equals(other.Value); + public bool Equals(RqlRule other) => this.Value.Equals(other.Value); public override string ToString() => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; @@ -80,11 +80,11 @@ internal string ToString(int indent) .ToString(); } - private static RqlAny ConvertCondition(IConditionNode condition) + private static RqlAny ConvertCondition(IConditionNode condition) { switch (condition) { - case ComposedConditionNode ccn: + case ComposedConditionNode ccn: var childConditions = new RqlArray(ccn.ChildConditionNodes.Count()); var i = 0; foreach (var childConditionNode in ccn.ChildConditionNodes) @@ -99,10 +99,10 @@ private static RqlAny ConvertCondition(IConditionNode condition) }; return new RqlReadOnlyObject(composedConditionProperties); - case ValueConditionNode vcn: + case ValueConditionNode vcn: var valueConditionProperties = new Dictionary(StringComparer.Ordinal) { - { "ConditionType", new RqlString(vcn.ConditionType.ToString()) }, + { "Condition", new RqlString(vcn.Condition) }, { "DataType", new RqlString(vcn.DataType.ToString()) }, { "LogicalOperator", new RqlString(vcn.LogicalOperator.ToString()) }, { "Operand", ConvertValue(vcn.Operand) }, diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs new file mode 100644 index 00000000..5d2aec8f --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs @@ -0,0 +1,29 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + + public readonly struct RqlRuleset : IRuntimeValue, IEquatable + { + private static readonly Type runtimeType = typeof(Ruleset); + + public RqlRuleset(Ruleset ruleset) + { + this.Value = ruleset; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => RqlTypes.Ruleset; + + public readonly Ruleset Value { get; } + + public static implicit operator RqlAny(RqlRuleset rqlRuleset) => new RqlAny(rqlRuleset); + + public bool Equals(RqlRuleset other) => this.Value.Equals(other.Value); + + public override string ToString() + => $"<{Type.Name}> {this.Value.Name}"; + } +} \ 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 index 39cb43a1..dd0dee0f 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlTypes.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlTypes.cs @@ -15,6 +15,7 @@ static RqlTypes() Object = new RqlType("object"); ReadOnlyObject = new RqlType("read_only_object"); Rule = new RqlType("rule"); + Ruleset = new RqlType("ruleset"); String = new RqlType("string"); // Register assignables. @@ -27,6 +28,7 @@ static RqlTypes() Object.AddAssignableType(Any); ReadOnlyObject.AddAssignableType(Any); Rule.AddAssignableType(Any); + Ruleset.AddAssignableType(Any); String.AddAssignableType(Any); } @@ -50,6 +52,8 @@ static RqlTypes() public static RqlType Rule { get; } + public static RqlType Ruleset { get; } + public static RqlType String { get; } } } \ No newline at end of file diff --git a/src/Rules.Framework/AssemblyMetadata.cs b/src/Rules.Framework/AssemblyMetadata.cs index c5c22406..8486a573 100644 --- a/src/Rules.Framework/AssemblyMetadata.cs +++ b/src/Rules.Framework/AssemblyMetadata.cs @@ -4,4 +4,5 @@ [assembly: InternalsVisibleTo("Rules.Framework.IntegrationTests")] [assembly: InternalsVisibleTo("Rules.Framework.Providers.InMemory.IntegrationTests")] [assembly: InternalsVisibleTo("Rules.Framework.Providers.MongoDb")] +[assembly: InternalsVisibleTo("Rules.Framework.Rql.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Rules.Framework/Builder/Generic/RuleBuilderResult.cs b/src/Rules.Framework/Builder/Generic/RuleBuilderResult.cs index 08b375c9..01cb9fb2 100644 --- a/src/Rules.Framework/Builder/Generic/RuleBuilderResult.cs +++ b/src/Rules.Framework/Builder/Generic/RuleBuilderResult.cs @@ -7,7 +7,7 @@ namespace Rules.Framework.Builder.Generic /// /// Contains the results information from a generic rule build operation. /// - public class RuleBuilderResult : RuleBuilderResultBase + public class RuleBuilderResult : RuleBuilderResultBase { /// /// Initializes a new instance of the class. @@ -15,7 +15,7 @@ public class RuleBuilderResult : RuleBuilderResult /// if set to true [is success]. /// The rule. /// The errors. - internal RuleBuilderResult(bool isSuccess, Rule rule, IEnumerable errors) + internal RuleBuilderResult(bool isSuccess, Rule rule, IEnumerable errors) : base(isSuccess, errors) { this.Rule = rule; @@ -25,7 +25,7 @@ internal RuleBuilderResult(bool isSuccess, Rule ru /// Gets the rule. /// /// The rule. - public Rule Rule { get; } + public Rule Rule { get; } /// /// Creates a result marked with failure. @@ -33,14 +33,14 @@ internal RuleBuilderResult(bool isSuccess, Rule ru /// The errors. /// /// errors - public static RuleBuilderResult Failure(IEnumerable errors) + public static RuleBuilderResult Failure(IEnumerable errors) { if (errors is null) { throw new System.ArgumentNullException(nameof(errors)); } - return new RuleBuilderResult(isSuccess: false, null!, errors); + return new RuleBuilderResult(isSuccess: false, null!, errors); } /// @@ -49,14 +49,14 @@ public static RuleBuilderResult Failure(IEnumerabl /// The rule. /// /// rule - public static RuleBuilderResult Success(Rule rule) + public static RuleBuilderResult Success(Rule rule) { if (rule is null) { throw new System.ArgumentNullException(nameof(rule)); } - return new RuleBuilderResult(isSuccess: true, rule, Enumerable.Empty()); + return new RuleBuilderResult(isSuccess: true, rule, Enumerable.Empty()); } } } \ No newline at end of file diff --git a/src/Rules.Framework/Builder/Generic/RulesBuilder/RuleBuilder.cs b/src/Rules.Framework/Builder/Generic/RulesBuilder/RuleBuilder.cs index de5700b3..4b8900d3 100644 --- a/src/Rules.Framework/Builder/Generic/RulesBuilder/RuleBuilder.cs +++ b/src/Rules.Framework/Builder/Generic/RulesBuilder/RuleBuilder.cs @@ -37,10 +37,10 @@ public IRuleBuilder ApplyWhen(Func ApplyWhen( - TCondition conditionType, Operators condOperator, TDataType operand) + TCondition condition, Operators condOperator, TDataType operand) { var rootConditionNodeBuilder = new RootConditionNodeBuilder(); - var valueCondition = rootConditionNodeBuilder.Value(conditionType, condOperator, operand); + var valueCondition = rootConditionNodeBuilder.Value(condition, condOperator, operand); return this.ApplyWhen(valueCondition); } diff --git a/src/Rules.Framework/Builder/RulesBuilder/FluentConditionNodeBuilder.cs b/src/Rules.Framework/Builder/RulesBuilder/FluentConditionNodeBuilder.cs index db9c3fab..473ea845 100644 --- a/src/Rules.Framework/Builder/RulesBuilder/FluentConditionNodeBuilder.cs +++ b/src/Rules.Framework/Builder/RulesBuilder/FluentConditionNodeBuilder.cs @@ -4,7 +4,6 @@ namespace Rules.Framework.Builder.RulesBuilder using System.Collections.Generic; using Rules.Framework; using Rules.Framework.ConditionNodes; - using Rules.Framework.Generic; internal sealed class FluentConditionNodeBuilder : IFluentConditionNodeBuilder { @@ -46,8 +45,7 @@ public IFluentConditionNodeBuilder Or( public IFluentConditionNodeBuilder Value(string condition, Operators condOperator, T operand) { - var conditionTypeAsString = GenericConversions.Convert(condition); - var valueConditionNode = ConditionNodeFactory.CreateValueNode(conditionTypeAsString, condOperator, operand); + var valueConditionNode = ConditionNodeFactory.CreateValueNode(condition, condOperator, operand); this.conditions.Add(valueConditionNode); return this; } diff --git a/src/Rules.Framework/Builder/Validation/GenericConditionNodeValidationArgs.cs b/src/Rules.Framework/Builder/Validation/GenericConditionNodeValidationArgs.cs index c3785a30..afaee018 100644 --- a/src/Rules.Framework/Builder/Validation/GenericConditionNodeValidationArgs.cs +++ b/src/Rules.Framework/Builder/Validation/GenericConditionNodeValidationArgs.cs @@ -2,10 +2,10 @@ namespace Rules.Framework.Builder.Validation { using FluentValidation; - internal sealed class GenericConditionNodeValidationArgs + internal sealed class GenericConditionNodeValidationArgs { - public GenericComposedConditionNodeValidator ComposedConditionNodeValidator { get; set; } + public GenericComposedConditionNodeValidator ComposedConditionNodeValidator { get; set; } public ValidationContext ValidationContext { get; set; } - public GenericValueConditionNodeValidator ValueConditionNodeValidator { get; set; } + public GenericValueConditionNodeValidator ValueConditionNodeValidator { get; set; } } } \ No newline at end of file diff --git a/src/Rules.Framework/Builder/Validation/GenericRuleValidator.cs b/src/Rules.Framework/Builder/Validation/GenericRuleValidator.cs index 5ee00a96..a089a6c8 100644 --- a/src/Rules.Framework/Builder/Validation/GenericRuleValidator.cs +++ b/src/Rules.Framework/Builder/Validation/GenericRuleValidator.cs @@ -3,19 +3,19 @@ namespace Rules.Framework.Builder.Validation using FluentValidation; using Rules.Framework.Generic; - internal sealed class GenericRuleValidator : AbstractValidator> + internal sealed class GenericRuleValidator : AbstractValidator> { - private static GenericRuleValidator ruleValidator; + private static GenericRuleValidator ruleValidator; - private readonly GenericComposedConditionNodeValidator composedConditionNodeValidator; + private readonly GenericComposedConditionNodeValidator composedConditionNodeValidator; - private readonly GenericValueConditionNodeValidator valueConditionNodeValidator; + private readonly GenericValueConditionNodeValidator valueConditionNodeValidator; private GenericRuleValidator() { - this.composedConditionNodeValidator = new GenericComposedConditionNodeValidator(); - this.valueConditionNodeValidator = new GenericValueConditionNodeValidator(); - this.RuleFor(r => r.RootCondition).Custom((cn, cc) => cn.PerformValidation(new GenericConditionNodeValidationArgs> + this.composedConditionNodeValidator = new GenericComposedConditionNodeValidator(); + this.valueConditionNodeValidator = new GenericValueConditionNodeValidator(); + this.RuleFor(r => r.RootCondition).Custom((cn, cc) => cn.PerformValidation(new GenericConditionNodeValidationArgs> { ComposedConditionNodeValidator = this.composedConditionNodeValidator, ValidationContext = cc, @@ -23,11 +23,11 @@ private GenericRuleValidator() })); } - public static GenericRuleValidator Instance + public static GenericRuleValidator Instance { get { - ruleValidator ??= new GenericRuleValidator(); + ruleValidator ??= new GenericRuleValidator(); return ruleValidator; } diff --git a/src/Rules.Framework/Core/PropertiesDictionary.cs b/src/Rules.Framework/Core/PropertiesDictionary.cs index 75904f27..99b1ac67 100644 --- a/src/Rules.Framework/Core/PropertiesDictionary.cs +++ b/src/Rules.Framework/Core/PropertiesDictionary.cs @@ -4,10 +4,9 @@ namespace Rules.Framework.Core using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; - using Rules.Framework.Generic; /// - /// A dictionary to hold the properties of a . + /// A dictionary to hold the properties of a . /// /// public class PropertiesDictionary : IDictionary diff --git a/src/Rules.Framework/Evaluation/Compiled/ConditionsValueLookupExtension.cs b/src/Rules.Framework/Evaluation/Compiled/ConditionsValueLookupExtension.cs index 5e403d04..17c51f5b 100644 --- a/src/Rules.Framework/Evaluation/Compiled/ConditionsValueLookupExtension.cs +++ b/src/Rules.Framework/Evaluation/Compiled/ConditionsValueLookupExtension.cs @@ -6,9 +6,9 @@ namespace Rules.Framework.Evaluation.Compiled internal static class ConditionsValueLookupExtension { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static object GetValueOrDefault(IDictionary conditions, string conditionType) + public static object GetValueOrDefault(IDictionary conditions, string conditionName) { - if (conditions.TryGetValue(conditionType, out var conditionValue)) + if (conditions.TryGetValue(conditionName, out var conditionValue)) { return conditionValue; } diff --git a/src/Rules.Framework/Generic/ConditionNodes/ValueConditionNode.cs b/src/Rules.Framework/Generic/ConditionNodes/ValueConditionNode.cs index 866ea61b..0f94aa83 100644 --- a/src/Rules.Framework/Generic/ConditionNodes/ValueConditionNode.cs +++ b/src/Rules.Framework/Generic/ConditionNodes/ValueConditionNode.cs @@ -17,7 +17,7 @@ public class ValueConditionNode : IValueConditionNode private readonly ValueConditionNode valueConditionNode; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The value condition node. public ValueConditionNode(ValueConditionNode valueConditionNode) diff --git a/src/Rules.Framework/IRulesDataSource.cs b/src/Rules.Framework/IRulesDataSource.cs index 94fc9c9a..331b838e 100644 --- a/src/Rules.Framework/IRulesDataSource.cs +++ b/src/Rules.Framework/IRulesDataSource.cs @@ -5,7 +5,7 @@ namespace Rules.Framework using System.Threading.Tasks; /// - /// Exposes the interface contract for a rules data source for specified content type. + /// Exposes the interface contract for a rules data source. /// public interface IRulesDataSource { @@ -24,14 +24,14 @@ public interface IRulesDataSource Task CreateRulesetAsync(string ruleset); /// - /// Gets the rules categorized with specified between - /// and . + /// Gets the rules categorized with specified between and . /// - /// the content type categorization. + /// the ruleset categorization. /// the filtering begin date. /// the filtering end date. /// - Task> GetRulesAsync(string contentType, DateTime dateBegin, DateTime dateEnd); + Task> GetRulesAsync(string ruleset, DateTime dateBegin, DateTime dateEnd); /// /// Gets the rules filtered by specified arguments. diff --git a/src/Rules.Framework/Providers/InMemory/IInMemoryRulesStorage.cs b/src/Rules.Framework/Providers/InMemory/IInMemoryRulesStorage.cs index f2bf79e1..3e61f26c 100644 --- a/src/Rules.Framework/Providers/InMemory/IInMemoryRulesStorage.cs +++ b/src/Rules.Framework/Providers/InMemory/IInMemoryRulesStorage.cs @@ -7,7 +7,7 @@ internal interface IInMemoryRulesStorage { void AddRule(RuleDataModel ruleDataModel); - void CreateRuleset(string contentType); + void CreateRuleset(string ruleset); IReadOnlyCollection GetAllRules(); diff --git a/src/Rules.Framework/Providers/InMemory/InMemoryRulesStorage.cs b/src/Rules.Framework/Providers/InMemory/InMemoryRulesStorage.cs index 1db0c3b8..d6873e9c 100644 --- a/src/Rules.Framework/Providers/InMemory/InMemoryRulesStorage.cs +++ b/src/Rules.Framework/Providers/InMemory/InMemoryRulesStorage.cs @@ -17,16 +17,16 @@ public InMemoryRulesStorage() public void AddRule(RuleDataModel ruleDataModel) { - var contentTypeRules = this.GetRulesCollectionByRuleset(ruleDataModel.Ruleset); + var rulesetRules = this.GetRulesCollectionByRuleset(ruleDataModel.Ruleset); - lock (contentTypeRules) + lock (rulesetRules) { - if (contentTypeRules.Exists(r => string.Equals(r.Name, ruleDataModel.Name, StringComparison.Ordinal))) + if (rulesetRules.Exists(r => string.Equals(r.Name, ruleDataModel.Name, StringComparison.Ordinal))) { throw new InvalidOperationException($"Rule with name '{ruleDataModel.Name}' already exists."); } - contentTypeRules.Add(ruleDataModel); + rulesetRules.Add(ruleDataModel); } } @@ -43,9 +43,9 @@ public void CreateRuleset(string ruleset) public IReadOnlyCollection GetAllRules() => this.rulesets.SelectMany(kvp => kvp.Value.Rules).ToList().AsReadOnly(); - public IReadOnlyCollection GetRulesBy(string contentType) + public IReadOnlyCollection GetRulesBy(string ruleset) { - var rules = this.GetRulesCollectionByRuleset(contentType); + var rules = this.GetRulesCollectionByRuleset(ruleset); return rules.AsReadOnly(); } @@ -55,18 +55,18 @@ public IReadOnlyCollection GetRulesets() public void UpdateRule(RuleDataModel ruleDataModel) { - var contentTypeRules = this.GetRulesCollectionByRuleset(ruleDataModel.Ruleset); + var rulesetRules = this.GetRulesCollectionByRuleset(ruleDataModel.Ruleset); - lock (contentTypeRules) + lock (rulesetRules) { - var existent = contentTypeRules.Find(r => string.Equals(r.Name, ruleDataModel.Name, StringComparison.Ordinal)); + var existent = rulesetRules.Find(r => string.Equals(r.Name, ruleDataModel.Name, StringComparison.Ordinal)); if (existent is null) { throw new InvalidOperationException($"Rule with name '{ruleDataModel.Name}' does not exist, no update can be done."); } - contentTypeRules.Remove(existent); - contentTypeRules.Add(ruleDataModel); + rulesetRules.Remove(existent); + rulesetRules.Add(ruleDataModel); } } diff --git a/src/Rules.Framework/RulesEngine.cs b/src/Rules.Framework/RulesEngine.cs index 9a686e1a..ade24f9a 100644 --- a/src/Rules.Framework/RulesEngine.cs +++ b/src/Rules.Framework/RulesEngine.cs @@ -79,8 +79,8 @@ public async Task CreateRulesetAsync(string ruleset) throw new ArgumentNullException(nameof(ruleset)); } - var getContentTypesArgs = new GetRulesetsArgs(); - var existentRulesets = await this.rulesSource.GetRulesetsAsync(getContentTypesArgs).ConfigureAwait(false); + var getRulesetArgs = new GetRulesetsArgs(); + var existentRulesets = await this.rulesSource.GetRulesetsAsync(getRulesetArgs).ConfigureAwait(false); if (existentRulesets.Any(rs => string.Equals(rs.Name, ruleset, StringComparison.Ordinal))) { return OperationResult.Failure($"The ruleset '{ruleset}' already exists."); @@ -118,9 +118,9 @@ public async Task> GetUniqueConditionsAsync(string ruleset, var getRulesArgs = new GetRulesArgs { - ContentType = ruleset, DateBegin = dateBegin, DateEnd = dateEnd, + Ruleset = ruleset, }; var matchedRules = await this.rulesSource.GetRulesAsync(getRulesArgs).ConfigureAwait(false); @@ -147,9 +147,9 @@ public async Task> MatchManyAsync( var getRulesArgs = new GetRulesArgs { - ContentType = ruleset, DateBegin = matchDateTime, DateEnd = matchDateTime, + Ruleset = ruleset, }; var orderedRules = await this.GetRulesOrderedAscendingAsync(getRulesArgs).ConfigureAwait(false); @@ -175,9 +175,9 @@ public async Task MatchOneAsync( var getRulesArgs = new GetRulesArgs { - ContentType = ruleset, DateBegin = matchDateTime, DateEnd = matchDateTime, + Ruleset = ruleset, }; var orderedRules = await this.GetRulesOrderedAscendingAsync(getRulesArgs).ConfigureAwait(false); @@ -219,9 +219,9 @@ public async Task> SearchAsync(SearchArgs sear var getRulesArgs = new GetRulesArgs { - ContentType = searchArgs.Ruleset, DateBegin = searchArgs.DateBegin, DateEnd = searchArgs.DateEnd, + Ruleset = searchArgs.Ruleset, }; var orderedRules = await this.GetRulesOrderedAscendingAsync(getRulesArgs).ConfigureAwait(false); @@ -373,8 +373,8 @@ await ManagementOperations.Manage(existentRules) private async Task CreateRulesetInternalAsync(string ruleset) { - var createContentTypeArgs = new CreateRulesetArgs { Name = ruleset }; - await this.rulesSource.CreateRulesetAsync(createContentTypeArgs).ConfigureAwait(false); + var createRulesetArgs = new CreateRulesetArgs { Name = ruleset }; + await this.rulesSource.CreateRulesetAsync(createRulesetArgs).ConfigureAwait(false); return OperationResult.Success(); } diff --git a/src/Rules.Framework/Serialization/IContentSerializationProvider.cs b/src/Rules.Framework/Serialization/IContentSerializationProvider.cs index bf88bf96..2d884ce8 100644 --- a/src/Rules.Framework/Serialization/IContentSerializationProvider.cs +++ b/src/Rules.Framework/Serialization/IContentSerializationProvider.cs @@ -2,16 +2,15 @@ namespace Rules.Framework.Serialization { /// /// Defines the interface contract for a content serialization provider. Provides content - /// serializers per content type value, allowing for customization of serializers per each - /// content type. + /// serializers per ruleset value, allowing for customization of serializers per each ruleset. /// public interface IContentSerializationProvider { /// - /// Gets the content serializer associated with the given . + /// Gets the content serializer associated with the given . /// - /// the content type. - /// the content serializer to deal with contents for specified content type. - IContentSerializer GetContentSerializer(string contentType); + /// the ruleset name. + /// the content serializer to deal with contents for the specified ruleset. + IContentSerializer GetContentSerializer(string ruleset); } } \ No newline at end of file diff --git a/src/Rules.Framework/Serialization/SerializedContentContainer.cs b/src/Rules.Framework/Serialization/SerializedContentContainer.cs index 47d1cddb..c6564a99 100644 --- a/src/Rules.Framework/Serialization/SerializedContentContainer.cs +++ b/src/Rules.Framework/Serialization/SerializedContentContainer.cs @@ -9,14 +9,14 @@ public class SerializedContentContainer : ContentContainer /// /// Creates a new . /// - /// the content type. + /// the ruleset name. /// the serialized content. /// the content serialization provider. public SerializedContentContainer( - string contentType, + string ruleset, object serializedContent, IContentSerializationProvider contentSerializationProvider) - : base((t) => contentSerializationProvider.GetContentSerializer(contentType).Deserialize(serializedContent, t)) + : base((t) => contentSerializationProvider.GetContentSerializer(ruleset).Deserialize(serializedContent, t)) { } } diff --git a/src/Rules.Framework/Source/GetRulesArgs.cs b/src/Rules.Framework/Source/GetRulesArgs.cs index 1d7cc200..51bc52f7 100644 --- a/src/Rules.Framework/Source/GetRulesArgs.cs +++ b/src/Rules.Framework/Source/GetRulesArgs.cs @@ -4,10 +4,10 @@ namespace Rules.Framework.Source internal sealed class GetRulesArgs { - public string ContentType { get; set; } - public DateTime DateBegin { get; set; } public DateTime DateEnd { get; set; } + + public string Ruleset { get; set; } } } \ No newline at end of file diff --git a/src/Rules.Framework/Source/RulesSource.cs b/src/Rules.Framework/Source/RulesSource.cs index 554b8b5a..c02682e1 100644 --- a/src/Rules.Framework/Source/RulesSource.cs +++ b/src/Rules.Framework/Source/RulesSource.cs @@ -170,7 +170,7 @@ private static GetRulesDelegate CreateGetRulesPipelineDelegate( { GetRulesDelegate action = async (args) - => await rulesDataSource.GetRulesAsync(args.ContentType, args.DateBegin, args.DateEnd).ConfigureAwait(false); + => await rulesDataSource.GetRulesAsync(args.Ruleset, args.DateBegin, args.DateEnd).ConfigureAwait(false); if (middlewares.Count > 0) { diff --git a/src/Rules.Framework/Validation/SearchArgsValidator.cs b/src/Rules.Framework/Validation/SearchArgsValidator.cs index 0ecad407..128f1886 100644 --- a/src/Rules.Framework/Validation/SearchArgsValidator.cs +++ b/src/Rules.Framework/Validation/SearchArgsValidator.cs @@ -5,22 +5,22 @@ namespace Rules.Framework.Validation internal sealed class SearchArgsValidator : AbstractValidator> { - private readonly Type conditionTypeRuntimeType; - private readonly Type contentTypeRuntimeType; + private readonly Type conditionRuntimeType; + private readonly Type rulesetRuntimeType; public SearchArgsValidator() { - this.conditionTypeRuntimeType = typeof(TCondition); - this.contentTypeRuntimeType = typeof(TRuleset); + this.conditionRuntimeType = typeof(TCondition); + this.rulesetRuntimeType = typeof(TRuleset); this.RuleFor(searchArgs => searchArgs.Ruleset).Must(ct => { - if (this.contentTypeRuntimeType.IsClass && ct is null) + if (this.rulesetRuntimeType.IsClass && ct is null) { return false; } - if (this.contentTypeRuntimeType.IsEnum && !Enum.IsDefined(this.contentTypeRuntimeType, ct)) + if (this.rulesetRuntimeType.IsEnum && !Enum.IsDefined(this.rulesetRuntimeType, ct)) { return false; } @@ -38,12 +38,12 @@ public SearchArgsValidator() conditionValidator.RuleFor(condition => condition.Key) .Must(conditionKey => { - if (this.conditionTypeRuntimeType.IsClass && conditionKey is null) + if (this.conditionRuntimeType.IsClass && conditionKey is null) { return false; } - if (this.conditionTypeRuntimeType.IsEnum && !Enum.IsDefined(this.conditionTypeRuntimeType, conditionKey)) + if (this.conditionRuntimeType.IsEnum && !Enum.IsDefined(this.conditionRuntimeType, conditionKey)) { return false; } diff --git a/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs b/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs index bd4effd9..c853735d 100644 --- a/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs +++ b/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs @@ -3,6 +3,7 @@ namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Rules.Framework.Generic; + using Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8; [SkewnessColumn, KurtosisColumn] public class Benchmark3 : IBenchmark diff --git a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionClass.cs b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionClass.cs new file mode 100644 index 00000000..3589880f --- /dev/null +++ b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionClass.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Tests.Stubs +{ + internal class ConditionClass + { + public int ConditionProperty1 { get; set; } + + public string ConditionProperty2 { get; set; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionTypeClass.cs b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionTypeClass.cs deleted file mode 100644 index 89225c77..00000000 --- a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ConditionTypeClass.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Rules.Framework.Tests.Stubs -{ - internal class ConditionTypeClass - { - public int ConditionTypeProperty1 { get; set; } - - public string ConditionTypeProperty2 { get; set; } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ContentTypeClass.cs b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ContentTypeClass.cs deleted file mode 100644 index 37ecd6c0..00000000 --- a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/ContentTypeClass.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Rules.Framework.Tests.Stubs -{ - public class ContentTypeClass - { - public int ContentTypeProperty1 { get; set; } - - public string ContentTypeProperty2 { get; set; } - } -} \ No newline at end of file diff --git a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/EmptyContentType.cs b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/EmptyRuleset.cs similarity index 61% rename from tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/EmptyContentType.cs rename to tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/EmptyRuleset.cs index 01f69af2..f2eff525 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/EmptyContentType.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/EmptyRuleset.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Tests.Stubs { - public enum EmptyContentType + public enum EmptyRuleset { } } \ No newline at end of file diff --git a/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/RulesetClass.cs b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/RulesetClass.cs new file mode 100644 index 00000000..8aa74e5f --- /dev/null +++ b/tests/Rules.Framework.IntegrationTests.Common/Features/Stubs/RulesetClass.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Tests.Stubs +{ + public class RulesetClass + { + public int RulesetProperty1 { get; set; } + + public string RulesetProperty2 { get; set; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs index 13e0c88d..6855945d 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs @@ -3,8 +3,8 @@ namespace Rules.Framework.BenchmarkTests.Tests.Benchmark1 using System; using System.Collections.Generic; using Rules.Framework; - using Rules.Framework.BenchmarkTests.Tests; using Rules.Framework.Generic; + 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 6cfc0a5c..bab54709 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.Generic; + using Rules.Framework.IntegrationTests.Common.Scenarios; public class Scenario7Data : IScenarioData { 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 d4f70aa1..e4545c7f 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs @@ -10,25 +10,25 @@ private IEnumerable> GetFlushRules() { return new[] { - Rule.Create("Benchmark 3 - Flush of Clubs") + Rule.Create("Scenario 8 - Flush of Clubs") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Flush" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfClubs, Operators.GreaterThanOrEqual, 5) .Build().Rule, - Rule.Create("Benchmark 3 - Flush of Diamonds") + Rule.Create("Scenario 8 - Flush of Diamonds") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Flush" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfDiamonds, Operators.GreaterThanOrEqual, 5) .Build().Rule, - Rule.Create("Benchmark 3 - Flush of Hearts") + Rule.Create("Scenario 8 - Flush of Hearts") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Flush" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfHearts, Operators.GreaterThanOrEqual, 5) .Build().Rule, - Rule.Create("Benchmark 3 - Flush of Spades") + Rule.Create("Scenario 8 - Flush of Spades") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Flush" }) .Since("2000-01-01") 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 88cff558..b31066e4 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs @@ -10,79 +10,79 @@ private IEnumerable> GetFourOfAKindRules() { return new[] { - Rule.Create("Benchmark 3 - Four Of A Kind Deuces") + Rule.Create("Scenario 8 - Four Of A Kind Deuces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfDeuces, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Treys") + Rule.Create("Scenario 8 - Four Of A Kind Treys") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTreys, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Fours") + Rule.Create("Scenario 8 - Four Of A Kind Fours") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfFours, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Fives") + Rule.Create("Scenario 8 - Four Of A Kind Fives") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfFives, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Sixes") + Rule.Create("Scenario 8 - Four Of A Kind Sixes") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSixes, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Sevens") + Rule.Create("Scenario 8 - Four Of A Kind Sevens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSevens, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Eights") + Rule.Create("Scenario 8 - Four Of A Kind Eights") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfEigths, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Nines") + Rule.Create("Scenario 8 - Four Of A Kind Nines") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfNines, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Tens") + Rule.Create("Scenario 8 - Four Of A Kind Tens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTens, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Jacks") + Rule.Create("Scenario 8 - Four Of A Kind Jacks") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfJacks, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Queens") + Rule.Create("Scenario 8 - Four Of A Kind Queens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfQueens, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Kings") + Rule.Create("Scenario 8 - Four Of A Kind Kings") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfKings, Operators.Equal, 4) .Build().Rule, - Rule.Create("Benchmark 3 - Four Of A Kind Aces") + Rule.Create("Scenario 8 - Four Of A Kind Aces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .Since("2000-01-01") 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 ecb4c598..441c6ba9 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs @@ -10,79 +10,79 @@ private IEnumerable> GetHighCardsRules() { return new[] { - Rule.Create("Benchmark 3 - High Card Deuces") + Rule.Create("Scenario 8 - High Card Deuces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfDeuces, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Treys") + Rule.Create("Scenario 8 - High Card Treys") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTreys, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Fours") + Rule.Create("Scenario 8 - High Card Fours") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfFours, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Fives") + Rule.Create("Scenario 8 - High Card Fives") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfFives, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Sixes") + Rule.Create("Scenario 8 - High Card Sixes") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSixes, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Sevens") + Rule.Create("Scenario 8 - High Card Sevens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSevens, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Eights") + Rule.Create("Scenario 8 - High Card Eights") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfEigths, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Nines") + Rule.Create("Scenario 8 - High Card Nines") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfNines, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Tens") + Rule.Create("Scenario 8 - High Card Tens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTens, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Jacks") + Rule.Create("Scenario 8 - High Card Jacks") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfJacks, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Queens") + Rule.Create("Scenario 8 - High Card Queens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfQueens, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Kings") + Rule.Create("Scenario 8 - High Card Kings") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfKings, Operators.Equal, 1) .Build().Rule, - Rule.Create("Benchmark 3 - High Card Aces") + Rule.Create("Scenario 8 - High Card Aces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "High Card" }) .Since("2000-01-01") 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 0f52a703..9b9557e6 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs @@ -10,79 +10,79 @@ private IEnumerable> GetPairsRules() { return new[] { - Rule.Create("Benchmark 3 - Pair Deuces") + Rule.Create("Scenario 8 - Pair Deuces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfDeuces, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Treys") + Rule.Create("Scenario 8 - Pair Treys") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTreys, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Fours") + Rule.Create("Scenario 8 - Pair Fours") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfFours, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Fives") + Rule.Create("Scenario 8 - Pair Fives") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfFives, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Sixes") + Rule.Create("Scenario 8 - Pair Sixes") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSixes, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Sevens") + Rule.Create("Scenario 8 - Pair Sevens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSevens, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Eights") + Rule.Create("Scenario 8 - Pair Eights") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfEigths, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Nines") + Rule.Create("Scenario 8 - Pair Nines") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfNines, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Tens") + Rule.Create("Scenario 8 - Pair Tens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTens, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Jacks") + Rule.Create("Scenario 8 - Pair Jacks") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfJacks, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Queens") + Rule.Create("Scenario 8 - Pair Queens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfQueens, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Kings") + Rule.Create("Scenario 8 - Pair Kings") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfKings, Operators.Equal, 2) .Build().Rule, - Rule.Create("Benchmark 3 - Pair Aces") + Rule.Create("Scenario 8 - Pair Aces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Pair" }) .Since("2000-01-01") 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 198d69ad..7277981c 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.RoyalFlush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.RoyalFlush.cs @@ -10,7 +10,7 @@ private IEnumerable> GetRoyalFlushRules() { return new[] { - Rule.Create("Benchmark 3 - Royal flush of Clubs: Ace, King, Queen, Jack, 10") + Rule.Create("Scenario 8 - Royal flush of Clubs: Ace, King, Queen, Jack, 10") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Royal Flush" }) .Since("2000-01-01") @@ -24,7 +24,7 @@ private IEnumerable> GetRoyalFlushRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Royal flush of Diamonds: Ace, King, Queen, Jack, 10") + Rule.Create("Scenario 8 - Royal flush of Diamonds: Ace, King, Queen, Jack, 10") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Royal Flush" }) .Since("2000-01-01") @@ -38,7 +38,7 @@ private IEnumerable> GetRoyalFlushRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Royal flush of Hearts: Ace, King, Queen, Jack, 10") + Rule.Create("Scenario 8 - Royal flush of Hearts: Ace, King, Queen, Jack, 10") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Royal Flush" }) .Since("2000-01-01") @@ -52,7 +52,7 @@ private IEnumerable> GetRoyalFlushRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Royal flush of Spades: Ace, King, Queen, Jack, 10") + Rule.Create("Scenario 8 - Royal flush of Spades: Ace, King, Queen, Jack, 10") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Royal Flush" }) .Since("2000-01-01") 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 d789d486..cd76ba82 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Straight.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Straight.cs @@ -10,7 +10,7 @@ private IEnumerable> GetStraightRules() { return new[] { - Rule.Create("Benchmark 3 - Straight 6, 5, 4, 3, 2") + Rule.Create("Scenario 8 - Straight 6, 5, 4, 3, 2") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") @@ -24,7 +24,7 @@ private IEnumerable> GetStraightRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight 7, 6, 5, 4, 3") + Rule.Create("Scenario 8 - Straight 7, 6, 5, 4, 3") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") @@ -38,7 +38,7 @@ private IEnumerable> GetStraightRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight 8, 7, 6, 5, 4") + Rule.Create("Scenario 8 - Straight 8, 7, 6, 5, 4") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") @@ -52,7 +52,7 @@ private IEnumerable> GetStraightRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight 9, 8, 7, 6, 5") + Rule.Create("Scenario 8 - Straight 9, 8, 7, 6, 5") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") @@ -66,7 +66,7 @@ private IEnumerable> GetStraightRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight 10, 9, 8, 7, 6") + Rule.Create("Scenario 8 - Straight 10, 9, 8, 7, 6") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") @@ -80,7 +80,7 @@ private IEnumerable> GetStraightRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight Jack, 10, 9, 8, 7") + Rule.Create("Scenario 8 - Straight Jack, 10, 9, 8, 7") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") @@ -94,7 +94,7 @@ private IEnumerable> GetStraightRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight Queen, Jack, 10, 9, 8") + Rule.Create("Scenario 8 - Straight Queen, Jack, 10, 9, 8") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") @@ -108,7 +108,7 @@ private IEnumerable> GetStraightRules() ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight King, Queen, Jack, 10, 9") + Rule.Create("Scenario 8 - Straight King, Queen, Jack, 10, 9") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight" }) .Since("2000-01-01") 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 19de203d..fbb4d024 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs @@ -11,7 +11,7 @@ private IEnumerable> GetStraightFlushRules( return new[] { // Straight flush of Clubs: - Rule.Create("Benchmark 3 - Straight flush of Clubs: 6, 5, 4, 3, 2") + Rule.Create("Scenario 8 - Straight flush of Clubs: 6, 5, 4, 3, 2") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -25,7 +25,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Clubs: 7, 6, 5, 4, 3") + Rule.Create("Scenario 8 - Straight flush of Clubs: 7, 6, 5, 4, 3") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -39,7 +39,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Clubs: 8, 7, 6, 5, 4") + Rule.Create("Scenario 8 - Straight flush of Clubs: 8, 7, 6, 5, 4") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -53,7 +53,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Clubs: 9, 8, 7, 6, 5") + Rule.Create("Scenario 8 - Straight flush of Clubs: 9, 8, 7, 6, 5") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -67,7 +67,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Clubs: 10, 9, 8, 7, 6") + Rule.Create("Scenario 8 - Straight flush of Clubs: 10, 9, 8, 7, 6") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -81,7 +81,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Clubs: Jack, 10, 9, 8, 7") + Rule.Create("Scenario 8 - Straight flush of Clubs: Jack, 10, 9, 8, 7") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -95,7 +95,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Clubs: Queen, Jack, 10, 9, 8") + Rule.Create("Scenario 8 - Straight flush of Clubs: Queen, Jack, 10, 9, 8") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -109,7 +109,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Clubs: King, Queen, Jack, 10, 9") + Rule.Create("Scenario 8 - Straight flush of Clubs: King, Queen, Jack, 10, 9") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -125,7 +125,7 @@ private IEnumerable> GetStraightFlushRules( .Build().Rule, // Straight flush of Diamonds: - Rule.Create("Benchmark 3 - Straight flush of Diamonds: 6, 5, 4, 3, 2") + Rule.Create("Scenario 8 - Straight flush of Diamonds: 6, 5, 4, 3, 2") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -139,7 +139,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Diamonds: 7, 6, 5, 4, 3") + Rule.Create("Scenario 8 - Straight flush of Diamonds: 7, 6, 5, 4, 3") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -153,7 +153,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Diamonds: 8, 7, 6, 5, 4") + Rule.Create("Scenario 8 - Straight flush of Diamonds: 8, 7, 6, 5, 4") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -167,7 +167,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Diamonds: 9, 8, 7, 6, 5") + Rule.Create("Scenario 8 - Straight flush of Diamonds: 9, 8, 7, 6, 5") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -181,7 +181,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Diamonds: 10, 9, 8, 7, 6") + Rule.Create("Scenario 8 - Straight flush of Diamonds: 10, 9, 8, 7, 6") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -195,7 +195,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Diamonds: Jack, 10, 9, 8, 7") + Rule.Create("Scenario 8 - Straight flush of Diamonds: Jack, 10, 9, 8, 7") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -209,7 +209,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Diamonds: Queen, Jack, 10, 9, 8") + Rule.Create("Scenario 8 - Straight flush of Diamonds: Queen, Jack, 10, 9, 8") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -223,7 +223,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Diamonds: King, Queen, Jack, 10, 9") + Rule.Create("Scenario 8 - Straight flush of Diamonds: King, Queen, Jack, 10, 9") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -239,7 +239,7 @@ private IEnumerable> GetStraightFlushRules( .Build().Rule, // Straight flush of Hearts: - Rule.Create("Benchmark 3 - Straight flush of Hearts: 6, 5, 4, 3, 2") + Rule.Create("Scenario 8 - Straight flush of Hearts: 6, 5, 4, 3, 2") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -253,7 +253,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Hearts: 7, 6, 5, 4, 3") + Rule.Create("Scenario 8 - Straight flush of Hearts: 7, 6, 5, 4, 3") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -267,7 +267,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Hearts: 8, 7, 6, 5, 4") + Rule.Create("Scenario 8 - Straight flush of Hearts: 8, 7, 6, 5, 4") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -281,7 +281,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Hearts: 9, 8, 7, 6, 5") + Rule.Create("Scenario 8 - Straight flush of Hearts: 9, 8, 7, 6, 5") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -295,7 +295,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Hearts: 10, 9, 8, 7, 6") + Rule.Create("Scenario 8 - Straight flush of Hearts: 10, 9, 8, 7, 6") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -309,7 +309,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Hearts: Jack, 10, 9, 8, 7") + Rule.Create("Scenario 8 - Straight flush of Hearts: Jack, 10, 9, 8, 7") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -323,7 +323,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Hearts: Queen, Jack, 10, 9, 8") + Rule.Create("Scenario 8 - Straight flush of Hearts: Queen, Jack, 10, 9, 8") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -337,7 +337,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Hearts: King, Queen, Jack, 10, 9") + Rule.Create("Scenario 8 - Straight flush of Hearts: King, Queen, Jack, 10, 9") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -353,7 +353,7 @@ private IEnumerable> GetStraightFlushRules( .Build().Rule, // Straight flush of Spades: - Rule.Create("Benchmark 3 - Straight flush of Spades: 6, 5, 4, 3, 2") + Rule.Create("Scenario 8 - Straight flush of Spades: 6, 5, 4, 3, 2") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -367,7 +367,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Spades: 7, 6, 5, 4, 3") + Rule.Create("Scenario 8 - Straight flush of Spades: 7, 6, 5, 4, 3") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -381,7 +381,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Spades: 8, 7, 6, 5, 4") + Rule.Create("Scenario 8 - Straight flush of Spades: 8, 7, 6, 5, 4") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -395,7 +395,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Spades: 9, 8, 7, 6, 5") + Rule.Create("Scenario 8 - Straight flush of Spades: 9, 8, 7, 6, 5") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -409,7 +409,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Spades: 10, 9, 8, 7, 6") + Rule.Create("Scenario 8 - Straight flush of Spades: 10, 9, 8, 7, 6") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -423,7 +423,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Spades: Jack, 10, 9, 8, 7") + Rule.Create("Scenario 8 - Straight flush of Spades: Jack, 10, 9, 8, 7") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -437,7 +437,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Spades: Queen, Jack, 10, 9, 8") + Rule.Create("Scenario 8 - Straight flush of Spades: Queen, Jack, 10, 9, 8") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") @@ -451,7 +451,7 @@ private IEnumerable> GetStraightFlushRules( ) ) .Build().Rule, - Rule.Create("Benchmark 3 - Straight flush of Spades: King, Queen, Jack, 10, 9") + Rule.Create("Scenario 8 - Straight flush of Spades: King, Queen, Jack, 10, 9") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Straight Flush" }) .Since("2000-01-01") 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 030d2671..f171db23 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs @@ -10,79 +10,79 @@ private IEnumerable> GetThreeOfAKindRules() { return new[] { - Rule.Create("Benchmark 3 - Three Of A Kind Deuces") + Rule.Create("Scenario 8 - Three Of A Kind Deuces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfDeuces, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Treys") + Rule.Create("Scenario 8 - Three Of A Kind Treys") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTreys, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Fours") + Rule.Create("Scenario 8 - Three Of A Kind Fours") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfDeuces, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Fives") + Rule.Create("Scenario 8 - Three Of A Kind Fives") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfFives, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Sixes") + Rule.Create("Scenario 8 - Three Of A Kind Sixes") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSixes, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Sevens") + Rule.Create("Scenario 8 - Three Of A Kind Sevens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfSevens, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Eights") + Rule.Create("Scenario 8 - Three Of A Kind Eights") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfEigths, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Nines") + Rule.Create("Scenario 8 - Three Of A Kind Nines") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfNines, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Tens") + Rule.Create("Scenario 8 - Three Of A Kind Tens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfTens, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Jacks") + Rule.Create("Scenario 8 - Three Of A Kind Jacks") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfJacks, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Queens") + Rule.Create("Scenario 8 - Three Of A Kind Queens") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfQueens, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Kings") + Rule.Create("Scenario 8 - Three Of A Kind Kings") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") .ApplyWhen(PokerConditions.NumberOfKings, Operators.Equal, 3) .Build().Rule, - Rule.Create("Benchmark 3 - Three Of A Kind Aces") + Rule.Create("Scenario 8 - Three Of A Kind Aces") .InRuleset(PokerRulesets.TexasHoldemPokerSingleCombinations) .SetContent(new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .Since("2000-01-01") diff --git a/tests/Rules.Framework.IntegrationTests/JsonContentSerializationProvider.cs b/tests/Rules.Framework.IntegrationTests/JsonContentSerializationProvider.cs index 52897fbd..56b04667 100644 --- a/tests/Rules.Framework.IntegrationTests/JsonContentSerializationProvider.cs +++ b/tests/Rules.Framework.IntegrationTests/JsonContentSerializationProvider.cs @@ -4,7 +4,7 @@ namespace Rules.Framework.IntegrationTests internal class JsonContentSerializationProvider : IContentSerializationProvider { - public IContentSerializer GetContentSerializer(string contentType) + public IContentSerializer GetContentSerializer(string ruleset) { return new JsonContentSerializer(); } diff --git a/tests/Rules.Framework.IntegrationTests/RulesFromJsonFile.cs b/tests/Rules.Framework.IntegrationTests/RulesFromJsonFile.cs index a90725cd..1576764f 100644 --- a/tests/Rules.Framework.IntegrationTests/RulesFromJsonFile.cs +++ b/tests/Rules.Framework.IntegrationTests/RulesFromJsonFile.cs @@ -25,15 +25,15 @@ public async Task FromJsonFileAsync(IRulesEngine>(contents); - var addedContentTypes = new HashSet(); + var addedRulesets = new HashSet(); foreach (var ruleDataModel in ruleDataModels) { - var contentType = GetRuleset(ruleDataModel.Ruleset); - if (!addedContentTypes.Contains(contentType)) + var ruleset = GetRuleset(ruleDataModel.Ruleset); + if (!addedRulesets.Contains(ruleset)) { - await rulesEngine.CreateRulesetAsync(contentType); - addedContentTypes.Add(contentType); + await rulesEngine.CreateRulesetAsync(ruleset); + addedRulesets.Add(ruleset); } object content; @@ -47,7 +47,7 @@ public async Task FromJsonFileAsync(IRulesEngine(ruleDataModel.Name) - .InRuleset(contentType) + .InRuleset(ruleset) .SetContent(content) .Since(ruleDataModel.DateBegin) .Until(ruleDataModel.DateEnd); diff --git a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs index 33035b00..5776794b 100644 --- a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs +++ b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs @@ -11,15 +11,15 @@ namespace Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngi public class OperatorContainsManyToOneTests : RulesEngineTestsBase { - private static readonly RulesetNames testContentType = RulesetNames.Sample1; + private static readonly RulesetNames testRuleset = RulesetNames.Sample1; private readonly Rule expectedMatchRule; private readonly Rule otherRule; public OperatorContainsManyToOneTests() - : base(testContentType) + : base(testRuleset) { this.expectedMatchRule = Rule.Create("Expected rule") - .InRuleset(testContentType) + .InRuleset(testRuleset) .SetContent("Just as expected!") .Since(UtcDate("2020-01-01Z")) .ApplyWhen(ConditionNames.Condition1, Operators.Contains, "Cat") @@ -27,7 +27,7 @@ public OperatorContainsManyToOneTests() .Rule; this.otherRule = Rule.Create("Other rule") - .InRuleset(testContentType) + .InRuleset(testRuleset) .SetContent("Oops! Not expected to be matched.") .Since(UtcDate("2020-01-01Z")) .Build() @@ -39,7 +39,7 @@ public OperatorContainsManyToOneTests() [Theory] [InlineData(false)] [InlineData(true)] - public async Task RulesEngine_GivenConditionType1WithArrayOfStringsContainingCat_MatchesExpectedRule(bool compiled) + public async Task RulesEngine_GivenCondition1WithArrayOfStringsContainingCat_MatchesExpectedRule(bool compiled) { // Arrange var emptyConditions = new Dictionary @@ -58,7 +58,7 @@ public async Task RulesEngine_GivenConditionType1WithArrayOfStringsContainingCat [Theory] [InlineData(false)] [InlineData(true)] - public async Task RulesEngine_GivenConditionType1WithArrayOfStringsNotContainingCat_MatchesOtherRule(bool compiled) + public async Task RulesEngine_GivenCondition1WithArrayOfStringsNotContainingCat_MatchesOtherRule(bool compiled) { // Arrange var emptyConditions = new Dictionary diff --git a/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario2/CarInsuranceAdvisorTests.cs b/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario2/CarInsuranceAdvisorTests.cs index 51fcdba9..878ef6d6 100644 --- a/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario2/CarInsuranceAdvisorTests.cs +++ b/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario2/CarInsuranceAdvisorTests.cs @@ -64,8 +64,8 @@ public CarInsuranceAdvisorTests() var mongoDatabase = this.mongoClient.GetDatabase(this.mongoDbProviderSettings.DatabaseName); mongoDatabase.DropCollection(this.mongoDbProviderSettings.RulesetsCollectionName); - var contentTypesMongoCollection = mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesetsCollectionName); - contentTypesMongoCollection.InsertMany(rulesets); + var rulesetsMongoCollection = mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesetsCollectionName); + rulesetsMongoCollection.InsertMany(rulesets); mongoDatabase.DropCollection(this.mongoDbProviderSettings.RulesCollectionName); var rulesMongoCollection = mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesCollectionName); diff --git a/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario3/BuildingSecuritySystemControlTests.cs b/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario3/BuildingSecuritySystemControlTests.cs index 5fb6d81b..7e65affc 100644 --- a/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario3/BuildingSecuritySystemControlTests.cs +++ b/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Scenarios/Scenario3/BuildingSecuritySystemControlTests.cs @@ -51,7 +51,7 @@ public BuildingSecuritySystemControlTests() }).ToList(); } - var contentTypes = rules + var rulesets = rules .Select(r => new RulesetDataModel { Creation = DateTime.UtcNow, @@ -64,8 +64,8 @@ public BuildingSecuritySystemControlTests() var mongoDatabase = this.mongoClient.GetDatabase(this.mongoDbProviderSettings.DatabaseName); mongoDatabase.DropCollection(this.mongoDbProviderSettings.RulesetsCollectionName); - var contentTypesMongoCollection = mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesetsCollectionName); - contentTypesMongoCollection.InsertMany(contentTypes); + var rulesetsMongoCollection = mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesetsCollectionName); + rulesetsMongoCollection.InsertMany(rulesets); mongoDatabase.DropCollection(this.mongoDbProviderSettings.RulesCollectionName); var rulesMongoCollection = mongoDatabase.GetCollection(this.mongoDbProviderSettings.RulesCollectionName); diff --git a/tests/Rules.Framework.Providers.MongoDb.Tests/MongoDbProviderRulesDataSourceTests.cs b/tests/Rules.Framework.Providers.MongoDb.Tests/MongoDbProviderRulesDataSourceTests.cs index db1c01f0..201cda2a 100644 --- a/tests/Rules.Framework.Providers.MongoDb.Tests/MongoDbProviderRulesDataSourceTests.cs +++ b/tests/Rules.Framework.Providers.MongoDb.Tests/MongoDbProviderRulesDataSourceTests.cs @@ -14,20 +14,20 @@ namespace Rules.Framework.Providers.MongoDb.Tests public class MongoDbProviderRulesDataSourceTests { [Fact] - public async Task CreateContentTypeAsync_GivenContentTypeName_InsertsContentTypeOnCollection() + public async Task CreateRulesetAsync_GivenRulesetName_InsertsRulesetOnCollection() { // Arrange - var contentType = nameof(RulesetNames.RulesetSample); + var ruleset = nameof(RulesetNames.RulesetSample); RulesetDataModel actual = null; - var contentTypesCollection = Mock.Of>(); - Mock.Get(contentTypesCollection) + var rulesetsCollection = Mock.Of>(); + Mock.Get(rulesetsCollection) .Setup(x => x.InsertOneAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((ct, _, _) => actual = ct); + .Callback((r, _, _) => actual = r); var mongoDatabase = Mock.Of(); Mock.Get(mongoDatabase) .Setup(x => x.GetCollection(It.IsAny(), null)) - .Returns(contentTypesCollection); + .Returns(rulesetsCollection); var mongoClient = Mock.Of(); Mock.Get(mongoClient) @@ -48,49 +48,55 @@ public async Task CreateContentTypeAsync_GivenContentTypeName_InsertsContentType ruleFactory); // Act - await mongoDbProviderRulesDataSource.CreateRulesetAsync(contentType); + await mongoDbProviderRulesDataSource.CreateRulesetAsync(ruleset); // Assert actual.Should().NotBeNull(); - actual.Name.Should().Be(contentType); + actual.Name.Should().Be(ruleset); actual.Id.Should().NotBeEmpty(); actual.Creation.Should().BeWithin(TimeSpan.FromSeconds(5)).Before(DateTime.UtcNow); } [Fact] - public async Task GetContentTypesAsync_NoConditions_ReturnsCollectionOfContentTypes() + public async Task GetRulesAsync_GivenRulesetAndDatesInterval_ReturnsCollectionOfRules() { // Arrange - var contentTypeDataModels = new[] + var ruleset = RulesetNames.RulesetSample.ToString(); + var dateBegin = new DateTime(2020, 03, 01); + var dateEnd = new DateTime(2020, 04, 01); + + var ruleDataModels = new List { - new RulesetDataModel + new() { - Creation = DateTime.UtcNow, - Id = Guid.NewGuid(), - Name = nameof(RulesetNames.RulesetSample), + Name = "Rule 1" }, + new() + { + Name = "Rule 2" + } }; - var fetchedRulesCursor = Mock.Of>(); + var fetchedRulesCursor = Mock.Of>(); Mock.Get(fetchedRulesCursor) .SetupSequence(x => x.MoveNextAsync(default)) .ReturnsAsync(true) .ReturnsAsync(false); Mock.Get(fetchedRulesCursor) .SetupGet(x => x.Current) - .Returns(contentTypeDataModels); + .Returns(ruleDataModels); Mock.Get(fetchedRulesCursor) .Setup(x => x.Dispose()); - var contentTypesCollection = Mock.Of>(); - Mock.Get(contentTypesCollection) - .Setup(x => x.FindAsync(It.IsAny>(), It.IsAny>(), default)) + var rulesCollection = Mock.Of>(); + Mock.Get(rulesCollection) + .Setup(x => x.FindAsync(It.IsAny>(), null, default)) .ReturnsAsync(fetchedRulesCursor); var mongoDatabase = Mock.Of(); Mock.Get(mongoDatabase) - .Setup(x => x.GetCollection(It.IsAny(), null)) - .Returns(contentTypesCollection); + .Setup(x => x.GetCollection(It.IsAny(), null)) + .Returns(rulesCollection); var mongoClient = Mock.Of(); Mock.Get(mongoClient) @@ -104,6 +110,14 @@ public async Task GetContentTypesAsync_NoConditions_ReturnsCollectionOfContentTy }; var ruleFactory = Mock.Of(); + Mock.Get(ruleFactory) + .Setup(x => x.CreateRule(It.IsAny())) + .Returns(x => Rule.Create(x.Name) + .InRuleset("test ruleset") + .SetContent(new object()) + .Since(dateBegin) + .Build() + .Rule); var mongoDbProviderRulesDataSource = new MongoDbProviderRulesDataSource( mongoClient, @@ -111,54 +125,47 @@ public async Task GetContentTypesAsync_NoConditions_ReturnsCollectionOfContentTy ruleFactory); // Act - var actual = await mongoDbProviderRulesDataSource.GetRulesetsAsync(); + var rules = await mongoDbProviderRulesDataSource.GetRulesAsync(ruleset, dateBegin, dateEnd); // Assert - actual.Should().NotBeNull() - .And.HaveCount(1) - .And.Contain(r => string.Equals(r.Name, nameof(RulesetNames.RulesetSample), StringComparison.Ordinal)); + rules.Should().NotBeNull() + .And.HaveCount(2); } [Fact] - public async Task GetRulesAsync_GivenContentTypeAndDatesInterval_ReturnsCollectionOfRules() + public async Task GetRulesetsAsync_NoConditions_ReturnsCollectionOfRulesets() { // Arrange - var contentType = RulesetNames.RulesetSample.ToString(); - var dateBegin = new DateTime(2020, 03, 01); - var dateEnd = new DateTime(2020, 04, 01); - - var ruleDataModels = new List + var rulesetDataModels = new[] { - new() + new RulesetDataModel { - Name = "Rule 1" + Creation = DateTime.UtcNow, + Id = Guid.NewGuid(), + Name = nameof(RulesetNames.RulesetSample), }, - new() - { - Name = "Rule 2" - } }; - var fetchedRulesCursor = Mock.Of>(); + var fetchedRulesCursor = Mock.Of>(); Mock.Get(fetchedRulesCursor) .SetupSequence(x => x.MoveNextAsync(default)) .ReturnsAsync(true) .ReturnsAsync(false); Mock.Get(fetchedRulesCursor) .SetupGet(x => x.Current) - .Returns(ruleDataModels); + .Returns(rulesetDataModels); Mock.Get(fetchedRulesCursor) .Setup(x => x.Dispose()); - var rulesCollection = Mock.Of>(); - Mock.Get(rulesCollection) - .Setup(x => x.FindAsync(It.IsAny>(), null, default)) + var rulesetsCollection = Mock.Of>(); + Mock.Get(rulesetsCollection) + .Setup(x => x.FindAsync(It.IsAny>(), It.IsAny>(), default)) .ReturnsAsync(fetchedRulesCursor); var mongoDatabase = Mock.Of(); Mock.Get(mongoDatabase) - .Setup(x => x.GetCollection(It.IsAny(), null)) - .Returns(rulesCollection); + .Setup(x => x.GetCollection(It.IsAny(), null)) + .Returns(rulesetsCollection); var mongoClient = Mock.Of(); Mock.Get(mongoClient) @@ -172,14 +179,6 @@ public async Task GetRulesAsync_GivenContentTypeAndDatesInterval_ReturnsCollecti }; var ruleFactory = Mock.Of(); - Mock.Get(ruleFactory) - .Setup(x => x.CreateRule(It.IsAny())) - .Returns(x => Rule.Create(x.Name) - .InRuleset("test ruleset") - .SetContent(new object()) - .Since(dateBegin) - .Build() - .Rule); var mongoDbProviderRulesDataSource = new MongoDbProviderRulesDataSource( mongoClient, @@ -187,11 +186,12 @@ public async Task GetRulesAsync_GivenContentTypeAndDatesInterval_ReturnsCollecti ruleFactory); // Act - var rules = await mongoDbProviderRulesDataSource.GetRulesAsync(contentType, dateBegin, dateEnd); + var actual = await mongoDbProviderRulesDataSource.GetRulesetsAsync(); // Assert - rules.Should().NotBeNull() - .And.HaveCount(2); + actual.Should().NotBeNull() + .And.HaveCount(1) + .And.Contain(r => string.Equals(r.Name, nameof(RulesetNames.RulesetSample), StringComparison.Ordinal)); } } } \ No newline at end of file diff --git a/tests/Rules.Framework.Providers.MongoDb.Tests/RuleFactoryTests.cs b/tests/Rules.Framework.Providers.MongoDb.Tests/RuleFactoryTests.cs index c98e5677..7c1ea4b6 100644 --- a/tests/Rules.Framework.Providers.MongoDb.Tests/RuleFactoryTests.cs +++ b/tests/Rules.Framework.Providers.MongoDb.Tests/RuleFactoryTests.cs @@ -99,7 +99,7 @@ public void CreateRule_GivenRuleDataModelWithComposedNodeAndChildNodesOfEachData var ruleDataModel = new RuleDataModel { Content = content, - Ruleset = "ContentTypeSample", + Ruleset = "RulesetSample", DateBegin = new System.DateTime(2020, 1, 1), DateEnd = null, Name = "My rule used for testing purposes", diff --git a/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializationProviderTests.cs b/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializationProviderTests.cs index 440d6db8..62a31d28 100644 --- a/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializationProviderTests.cs +++ b/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializationProviderTests.cs @@ -8,7 +8,7 @@ namespace Rules.Framework.Providers.MongoDb.Tests.Serialization public class DynamicToStrongTypeContentSerializationProviderTests { [Fact] - public void GetContentSerializer_GivenAnyContentTypeValue_ReturnsDynamicToStrongTypeContentSerializer() + public void GetContentSerializer_GivenAnyRulesetValue_ReturnsDynamicToStrongTypeContentSerializer() { // Arrange var dynamicToStrongTypeContentSerializationProvider = new DynamicToStrongTypeContentSerializationProvider(); diff --git a/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializerTests.cs b/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializerTests.cs index 7c32d48f..10202142 100644 --- a/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializerTests.cs +++ b/tests/Rules.Framework.Providers.MongoDb.Tests/Serialization/DynamicToStrongTypeContentSerializerTests.cs @@ -69,7 +69,7 @@ public void Deserialize_GivenNonDynamicSerializedContent_ThrowsNotSupportedExcep // Assert notSupportedException.Should().NotBeNull(); - notSupportedException.Message.Should().Be($"The serialized content type is not supported for deserialization: {typeof(object).FullName}"); + notSupportedException.Message.Should().Be($"The serialized ruleset is not supported for deserialization: {typeof(object).FullName}"); } [Fact] diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml index b7edaff8..defa6146 100644 --- a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml @@ -18,39 +18,39 @@ checks: - rql: "MATCH ONE RULE FOR" expectsSuccess: false expectedMessages: - - Expected content type name. - - rql: MATCH ONE RULE FOR "Test Content" + - Expected ruleset name. + - rql: MATCH ONE RULE FOR "Test Ruleset" expectsSuccess: false expectedMessages: - Expected token 'ON'. - - rql: MATCH ONE RULE FOR "Test Content" ON + - rql: MATCH ONE RULE FOR "Test Ruleset" ON expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$; + - rql: MATCH ONE RULE FOR "Test Ruleset" ON $2020-01-01$; expectsSuccess: true expectedMessages: [] - - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ + - rql: MATCH ONE RULE FOR "Test Ruleset" ON $2020-01-01$ expectsSuccess: false expectedMessages: - Expected token ';'. - - rql: MATCH ONE RULE FOR "Test Content" ON "Test not a date" + - rql: MATCH ONE RULE FOR "Test Ruleset" ON "Test not a date" expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: MATCH ONE RULE FOR "Test Content" ON 123 + - rql: MATCH ONE RULE FOR "Test Ruleset" ON 123 expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: MATCH ONE RULE FOR "Test Content" ON 16.8 + - rql: MATCH ONE RULE FOR "Test Ruleset" ON 16.8 expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: MATCH ONE RULE FOR "Test Content" ON false + - rql: MATCH ONE RULE FOR "Test Ruleset" ON false expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: MATCH ONE RULE FOR "Test Content" ON nothing + - rql: MATCH ONE RULE FOR "Test Ruleset" ON nothing expectsSuccess: false expectedMessages: - Expected literal of type date. @@ -60,54 +60,54 @@ checks: - 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. + - Literal 'TRUE' is not allowed as a valid ruleset. 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. + - Literal '13.1' is not allowed as a valid ruleset. 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 }; + - Literal 'NOTHING' is not allowed as a valid ruleset. Only literals of types [Integer, String] are allowed. + - rql: MATCH ONE RULE FOR "Test Ruleset" 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 } + - rql: MATCH ONE RULE FOR "Test Ruleset" 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; + - rql: MATCH ONE RULE FOR "Test Ruleset" ON $2020-01-01$ WHEN; expectsSuccess: false expectedMessages: - Expected '{' after WITH. - - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN {}; + - rql: MATCH ONE RULE FOR "Test Ruleset" 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 }; + - rql: MATCH ONE RULE FOR "Test Ruleset" 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 }; + - rql: MATCH ONE RULE FOR "Test Ruleset" 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$ }; + - rql: MATCH ONE RULE FOR "Test Ruleset" 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 }; + - rql: MATCH ONE RULE FOR "Test Ruleset" 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, }; + - rql: MATCH ONE RULE FOR "Test Ruleset" 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 + - rql: MATCH ONE RULE FOR "Test Ruleset" 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" }; + - rql: MATCH ONE RULE FOR "Test Ruleset" 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 index f206f518..26e68849 100644 --- a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/SearchExpressionChecks.yaml +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/SearchExpressionChecks.yaml @@ -14,47 +14,47 @@ checks: - rql: SEARCH RULES FOR expectsSuccess: false expectedMessages: - - Expected content type name. - - rql: SEARCH RULES FOR "Test Content" + - Expected ruleset name. + - rql: SEARCH RULES FOR "Test Ruleset" expectsSuccess: false expectedMessages: - Expected token 'SINCE'. - - rql: SEARCH RULES FOR "Test Content" SINCE + - rql: SEARCH RULES FOR "Test Ruleset" SINCE expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$; + - rql: SEARCH RULES FOR "Test Ruleset" SINCE $2020-01-01$; expectsSuccess: false expectedMessages: - Expected token 'UNTIL'. - - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL + - rql: SEARCH RULES FOR "Test Ruleset" 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$; + - rql: SEARCH RULES FOR "Test Ruleset" SINCE $2020-01-01$ UNTIL $2021-01-01$; expectsSuccess: true expectedMessages: [] - - rql: SEARCH RULES FOR "Test Content" SINCE "Test not a date" + - rql: SEARCH RULES FOR "Test Ruleset" 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" + - rql: SEARCH RULES FOR "Test Ruleset" 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 + - rql: SEARCH RULES FOR "Test Ruleset" SINCE 123 expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: SEARCH RULES FOR "Test Content" SINCE 16.8 + - rql: SEARCH RULES FOR "Test Ruleset" SINCE 16.8 expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: SEARCH RULES FOR "Test Content" SINCE false + - rql: SEARCH RULES FOR "Test Ruleset" SINCE false expectsSuccess: false expectedMessages: - Expected literal of type date. - - rql: SEARCH RULES FOR "Test Content" SINCE nothing + - rql: SEARCH RULES FOR "Test Ruleset" SINCE nothing expectsSuccess: false expectedMessages: - Expected literal of type date. @@ -64,58 +64,58 @@ checks: - 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. + - Literal 'TRUE' is not allowed as a valid ruleset. 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. + - Literal '13.1' is not allowed as a valid ruleset. 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 }; + - Literal 'NOTHING' is not allowed as a valid ruleset. Only literals of types [Integer, String] are allowed. + - rql: SEARCH RULES FOR "Test Ruleset" 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 } + - rql: SEARCH RULES FOR "Test Ruleset" 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; + - rql: SEARCH RULES FOR "Test Ruleset" 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 {}; + - rql: SEARCH RULES FOR "Test Ruleset" 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 }; + - rql: SEARCH RULES FOR "Test Ruleset" 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 }; + - rql: SEARCH RULES FOR "Test Ruleset" 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$ }; + - rql: SEARCH RULES FOR "Test Ruleset" 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 }; + - rql: SEARCH RULES FOR "Test Ruleset" 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, }; + - rql: SEARCH RULES FOR "Test Ruleset" 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 + - rql: SEARCH RULES FOR "Test Ruleset" 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" }; + - rql: SEARCH RULES FOR "Test Ruleset" 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; + - rql: SEARCH RULES FOR "Test Ruleset" 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/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs index 4a0d1a68..180442a9 100644 --- a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs @@ -9,8 +9,6 @@ public class RulesEngineWithScenario8RulesFixture : IDisposable public RulesEngineWithScenario8RulesFixture() { this.RulesEngine = RulesEngineBuilder.CreateRulesEngine() - .WithContentType() - .WithConditionType() .SetInMemoryDataSource() .Configure(options => { @@ -23,7 +21,7 @@ public RulesEngineWithScenario8RulesFixture() ScenarioLoader.LoadScenarioAsync(this.RulesEngine, scenarioData).GetAwaiter().GetResult(); } - public IRulesEngine RulesEngine { get; private set; } + public IRulesEngine RulesEngine { get; private set; } public void Dispose() { diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs index 5a50e6bb..9378e957 100644 --- a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs @@ -2,7 +2,6 @@ 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; @@ -35,12 +34,12 @@ public async Task PokerCombinations_GivenMatchAllRqlStatement_EvaluatesAndReturn if (expectsRules) { - result.Should().BeOfType>(); - var rulesSetResult = (RulesSetResult)result; + 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++) + for (var i = 0; i < ruleNames.Length; i++) { var rule = rulesSetResult.Lines[i].Rule.Value; rule.Name.Should().Be(ruleNames[i]); @@ -76,8 +75,8 @@ public async Task PokerCombinations_GivenMatchOneRqlStatement_EvaluatesAndReturn if (expectsRule) { - result.Should().BeOfType>(); - var rulesSetResult = (RulesSetResult)result; + result.Should().BeOfType(); + var rulesSetResult = (RulesSetResult)result; rulesSetResult.NumberOfRules.Should().Be(1); rulesSetResult.Lines.Should().HaveCount(1); @@ -114,12 +113,12 @@ public async Task PokerCombinations_GivenSearchRqlStatement_EvaluatesAndReturnsR if (expectsRules) { - result.Should().BeOfType>(); - var rulesSetResult = (RulesSetResult)result; + 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++) + for (var i = 0; i < ruleNames.Length; i++) { var rule = rulesSetResult.Lines[i].Rule.Value; rule.Name.Should().Be(ruleNames[i]); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs index 3c666a7e..27ea30a2 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs @@ -6,7 +6,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -21,13 +20,13 @@ public async Task VisitBinaryExpression_GivenValidBinaryExpression_ProcessesRule var rightExpression = CreateMockedExpression(NewRqlString("Hello world")); var binaryExpression = new BinaryExpression(leftExpression, operatorSegment, rightExpression); - var runtime = Mock.Of>(); + 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); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitBinaryExpression(binaryExpression); @@ -47,14 +46,14 @@ public async Task VisitBinaryExpression_GivenValidBinaryExpressionFailingBinaryO var rightExpression = CreateMockedExpression(NewRqlString("Hello world")); var binaryExpression = new BinaryExpression(leftExpression, operatorSegment, rightExpression); - var runtime = Mock.Of>(); + 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); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var interpreterException = await Assert.ThrowsAsync(async () => await interpreter.VisitBinaryExpression(binaryExpression)); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs index ef287f8b..d090d0fd 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs @@ -7,7 +7,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -21,10 +20,10 @@ public async Task VisitCardinalitySegment_GivenValidCardinalitySegment_ReturnsCa var ruleExpression = CreateMockedExpression(NewRqlString("rule")); var cardinalitySegment = CardinalitySegment.Create(cardinalityExpression, ruleExpression); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitCardinalitySegment(cardinalitySegment); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs index 0a61e62e..56a637ee 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs @@ -7,7 +7,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -21,13 +20,13 @@ public async Task VisitExpressionStatemet_GivenValidExpressionStatement_ReturnsE var expression = CreateMockedExpression(expectedValue); var expressionStatement = ExpressionStatement.Create(expression); - var runtime = Mock.Of>(); + 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); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitExpressionStatement(expressionStatement); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs index 70362b20..77aa8c1a 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs @@ -8,7 +8,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -21,10 +20,10 @@ public async Task VisitIdentifierExpression_GivenValidIdentifierExpression_Retur var identifierToken = NewToken("test", null, TokenType.IDENTIFIER); var identifierExpression = new IdentifierExpression(identifierToken); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitIdentifierExpression(identifierExpression); 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 633de90e..9ffaee38 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs @@ -4,67 +4,39 @@ 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; using Rules.Framework.Rql.Tests.Stubs; + using Rules.Framework.Rql.Tokens; 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.IsAny())) - .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 expectedCondition = nameof(Conditions.IsoCountryCode); var expectedConditionValue = "test"; var leftExpression = CreateMockedExpression(NewRqlString("IsoCountryCode")); - var operatorToken = NewToken("is", null, Framework.Rql.Tokens.TokenType.IS); + var operatorToken = NewToken("is", null, TokenType.IS); var rightExpression = CreateMockedExpression(NewRqlString(expectedConditionValue)); var inputConditionSegment = new InputConditionSegment(leftExpression, operatorToken, rightExpression); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + 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); + actual.Should().NotBeNull().And.BeOfType>(); + var actualCondition = (ValueTuple)actual; + actualCondition.Item1.Should().Be(expectedCondition); + actualCondition.Item2.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 index 1622d023..2ff888c9 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionsSegment.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionsSegment.cs @@ -17,24 +17,27 @@ public partial class InterpreterTests public async Task VisitInputConditionsSegment_GivenValidInputConditionsSegment_ReturnsConditionsCollection() { // Arrange - var expectedCondition1 = new Condition(ConditionType.IsoCountryCode, "PT"); - var expectedCondition2 = new Condition(ConditionType.IsVip, true); + var expectedCondition1 = new ValueTuple(nameof(Conditions.IsoCountryCode), "PT"); + var expectedCondition2 = new ValueTuple(nameof(Conditions.IsVip), true); var inputConditionSegment1 = CreateMockedSegment(expectedCondition1); var inputConditionSegment2 = CreateMockedSegment(expectedCondition2); var inputConditionsSegment = new InputConditionsSegment(new[] { inputConditionSegment1, inputConditionSegment2 }); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + 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); + actual.Should().NotBeNull().And.BeAssignableTo>(); + var actualConditions = actual as IDictionary; + actualConditions.Should() + .Contain(expectedCondition1.Item1, expectedCondition1.Item2) + .And + .Contain(expectedCondition2.Item1, expectedCondition2.Item2); } } } \ 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 index b7a5804f..76d8dbcc 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.KeywordExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.KeywordExpression.cs @@ -7,7 +7,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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; @@ -21,10 +20,10 @@ public async Task VisitKeywordExpression_GivenValidKeywordExpression_ReturnsLexe var keywordToken = NewToken("var", null, TokenType.VAR); var keywordExpression = KeywordExpression.Create(keywordToken); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitKeywordExpression(keywordExpression); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs index 7fe1b144..bf546c54 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs @@ -9,7 +9,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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; @@ -38,10 +37,10 @@ public async Task VisitLiteralExpression_GivenLiteralExpressionWithUnsupportedLi var literalToken = NewToken("dummy", "dummy", TokenType.IDENTIFIER); var literalExpression = LiteralExpression.Create((LiteralType)(-1), literalToken, "test"); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitLiteralExpression(literalExpression)); @@ -58,10 +57,10 @@ public async Task VisitLiteralExpression_GivenValidLiteralExpression_ReturnsRunt var literalToken = NewToken("dummy", expected, TokenType.IDENTIFIER); var literalExpression = LiteralExpression.Create((LiteralType)literalType, literalToken, runtimeValue); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitLiteralExpression(literalExpression); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs index f5abe0a3..bf84379b 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs @@ -28,72 +28,87 @@ public partial class InterpreterTests }; [Fact] - public async Task VisitMatchExpression_GivenInvalidMatchExpressionWithInvalidContentType_ThrowsInterpreterException() + public async Task VisitMatchExpression_GivenInvalidMatchExpressionWithInvalidRuleset_ThrowsInterpreterException() { // Arrange - var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + var conditions = new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + }; var cardinalitySegment = CreateMockedSegment(NewRqlString("one")); - var contentTypeExpression = CreateMockedExpression(NewRqlDecimal(1m)); + var rulesetExpression = 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 matchExpression = MatchExpression.Create(cardinalitySegment, rulesetExpression, matchDateExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + 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"); + actual.Message.Should().Contain("Expected a ruleset value of type 'string' but found 'decimal' instead"); } [Fact] - public async Task VisitMatchExpression_GivenInvalidMatchExpressionWithUnknownContentType_ThrowsInterpreterException() + public async Task VisitMatchExpression_GivenInvalidMatchExpressionWithUnknownRuleset_ThrowsInterpreterException() { // Arrange - var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + var conditions = new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + }; var cardinalitySegment = CreateMockedSegment(NewRqlString("one")); - var contentTypeExpression = CreateMockedExpression(NewRqlString("dummy")); + var rulesetExpression = 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 matchExpression = MatchExpression.Create(cardinalitySegment, rulesetExpression, matchDateExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); + Mock.Get(runtime) + .Setup(x => x.GetRulesetsAsync()) + .ReturnsAsync(NewRqlArray(new RqlRuleset(new Ruleset("other", DateTime.UtcNow)))); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + 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"); + actual.Message.Should().Contain("The ruleset 'dummy' was not found"); } [Fact] public async Task VisitMatchExpression_GivenMatchExpressionFailingRuntimeEvaluation_ThrowsInterpreterException() { // Arrange - var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + var conditions = new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + }; var cardinalitySegment = CreateMockedSegment(NewRqlString("one")); - var contentTypeExpression = CreateMockedExpression(NewRqlString("Type1")); + var rulesetExpression = 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 matchExpression = MatchExpression.Create(cardinalitySegment, rulesetExpression, matchDateExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); + Mock.Get(runtime) + .Setup(x => x.GetRulesetsAsync()) + .ReturnsAsync(NewRqlArray(new RqlRuleset(new Ruleset("Type1", DateTime.UtcNow)))); Mock.Get(runtime) - .Setup(x => x.MatchRulesAsync(It.IsAny>())) + .Setup(x => x.MatchRulesAsync(It.IsAny())) .Throws(new RuntimeException("test")); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitMatchExpression(matchExpression)); @@ -106,34 +121,42 @@ public async Task VisitMatchExpression_GivenMatchExpressionFailingRuntimeEvaluat [MemberData(nameof(ValidCasesMatchExpression))] public async Task VisitMatchExpression_GivenValidMatchExpressionForOneCardinality_ReturnsOneRule( string cardinalityName, - object contentTypeName, + object rulesetName, 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)) + var ruleResult = Rule.Create("Dummy rule") + .InRuleset(Rulesets.Type1) + .SetContent("test") + .Since(DateTime.Now) + .ApplyWhen(x => x.Value(Conditions.IsVip, Operators.Equal, false)) .Build(); - var conditions = hasConditions ? new[] { new Condition(ConditionType.IsVip, false) } : null; + var conditions = hasConditions + ? new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + } + : null; var expected = new RqlArray(1); - expected.SetAtIndex(0, new RqlRule(ruleResult.Rule)); + expected.SetAtIndex(0, new RqlRule(ruleResult.Rule)); var cardinalitySegment = CreateMockedSegment(NewRqlString(cardinalityName)); - var contentTypeExpression = CreateMockedExpression((IRuntimeValue)contentTypeName); + var rulesetExpression = CreateMockedExpression((IRuntimeValue)rulesetName); var matchDateExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); - var inputConditionsSegment = CreateMockedSegment(conditions); - var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + var inputConditionsSegment = CreateMockedSegment(conditions!); + var matchExpression = MatchExpression.Create(cardinalitySegment, rulesetExpression, matchDateExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); + Mock.Get(runtime) + .Setup(x => x.GetRulesetsAsync()) + .ReturnsAsync(NewRqlArray(new RqlRuleset(new Ruleset("Type1", DateTime.UtcNow)))); Mock.Get(runtime) - .Setup(x => x.MatchRulesAsync(It.IsAny>())) + .Setup(x => x.MatchRulesAsync(It.IsAny())) .Returns(new ValueTask(expected)); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitMatchExpression(matchExpression); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs index f7155384..42783d91 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs @@ -9,7 +9,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -26,10 +25,10 @@ public async Task VisitNewArrayExpression_GivenValidNewArrayExpressionWithSizeIn var newArrayExpression = NewArrayExpression.Create(arrayToken, initializerBeginToken, sizeExpression, values, initializerEndToken); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitNewArrayExpression(newArrayExpression); @@ -59,10 +58,10 @@ public async Task VisitNewArrayExpression_GivenValidNewArrayExpressionWithValues var newArrayExpression = NewArrayExpression.Create(arrayToken, initializerBeginToken, sizeExpression, values, initializerEndToken); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitNewArrayExpression(newArrayExpression); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs index ea72752f..43193592 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs @@ -9,7 +9,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -34,10 +33,10 @@ public async Task VisitNewObjectExpression_GivenValidNewObjectExpressionWithProp var newObjectExpression = new NewObjectExpression(objectToken, values); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitNewObjectExpression(newObjectExpression); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs index 3fbf0e35..18d223dd 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs @@ -10,7 +10,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -21,10 +20,10 @@ public async Task VisitNoneExpression_GivenNoneExpression_ReturnsRqlNothing() // Arrange var noneExpression = new NoneExpression(); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitNoneExpression(noneExpression); @@ -40,10 +39,10 @@ public async Task VisitNoneSegment_GivenNoneSegment_ReturnsNull() // Arrange var noneSegment = new NoneSegment(); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitNoneSegment(noneSegment); @@ -58,10 +57,10 @@ public async Task VisitNoneStatement_GivenNoneStatement_ReturnsExpressionStateme // Arrange var noneStatement = new NoneStatement(); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitNoneStatement(noneStatement); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs index 82ee92ac..e75e33de 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs @@ -10,7 +10,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -36,10 +35,10 @@ public async Task VisitOperatorSegment_GivenOperatorSegmentWithSupportedOperator // Arrange var operatorSegment = new OperatorSegment(tokenTypes.Select(tt => NewToken("test", null, (TokenType)tt)).ToArray()); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitOperatorSegment(operatorSegment); @@ -54,10 +53,10 @@ public async Task VisitOperatorSegment_GivenOperatorSegmentWithUnsupportedOperat // Arrange var operatorSegment = new OperatorSegment(new[] { NewToken("test", null, TokenType.NOT) }); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actualException = await Assert.ThrowsAsync(async () => await interpreter.VisitOperatorSegment(operatorSegment)); @@ -74,10 +73,10 @@ public async Task VisitOperatorSegment_GivenOperatorSegmentWithUnsupportedOperat // Arrange var operatorSegment = new OperatorSegment(new[] { NewToken("test", null, (TokenType)tokenType1), NewToken("test", null, (TokenType)tokenType2) }); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actualException = await Assert.ThrowsAsync(async () => await interpreter.VisitOperatorSegment(operatorSegment)); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs index 24b586b3..f85f0c67 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs @@ -1,9 +1,5 @@ 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; @@ -12,7 +8,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -23,10 +18,10 @@ public async Task VisitPlaceholderExpression_GivenPlaceholderExpression_ReturnsR // Arrange var placeholderExpression = new PlaceholderExpression(NewToken("testPlaceholder", "testPlaceholder", Framework.Rql.Tokens.TokenType.PLACEHOLDER)); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitPlaceholderExpression(placeholderExpression); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs index e744a0ee..c1f0915d 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs @@ -24,72 +24,87 @@ public partial class InterpreterTests }; [Fact] - public async Task VisitSearchExpression_GivenInvalidSearchExpressionWithInvalidContentType_ThrowsInterpreterException() + public async Task VisitSearchExpression_GivenInvalidSearchExpressionWithInvalidRuleset_ThrowsInterpreterException() { // Arrange - var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + var conditions = new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + }; - var contentTypeExpression = CreateMockedExpression(NewRqlDecimal(1m)); + var rulesetExpression = 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 searchExpression = new SearchExpression(rulesetExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + 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"); + actual.Message.Should().Contain("Expected a ruleset value of type 'string' but found 'decimal' instead"); } [Fact] - public async Task VisitSearchExpression_GivenInvalidSearchExpressionWithUnknownContentType_ThrowsInterpreterException() + public async Task VisitSearchExpression_GivenInvalidSearchExpressionWithUnknownRuleset_ThrowsInterpreterException() { // Arrange - var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + var conditions = new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + }; - var contentTypeExpression = CreateMockedExpression(NewRqlString("dummy")); + var rulesetExpression = 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 searchExpression = new SearchExpression(rulesetExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); + Mock.Get(runtime) + .Setup(x => x.GetRulesetsAsync()) + .ReturnsAsync(NewRqlArray(new RqlRuleset(new Ruleset("other", DateTime.UtcNow)))); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + 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"); + actual.Message.Should().Contain("The ruleset 'dummy' was not found"); } [Fact] public async Task VisitSearchExpression_GivenSearchExpressionFailingRuntimeEvaluation_ThrowsInterpreterException() { // Arrange - var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + var conditions = new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + }; - var contentTypeExpression = CreateMockedExpression(NewRqlString("Type1")); + var rulesetExpression = 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 searchExpression = new SearchExpression(rulesetExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); + Mock.Get(runtime) + .Setup(x => x.GetRulesetsAsync()) + .ReturnsAsync(NewRqlArray(new RqlRuleset(new Ruleset("Type1", DateTime.UtcNow)))); Mock.Get(runtime) - .Setup(x => x.SearchRulesAsync(It.IsAny>())) + .Setup(x => x.SearchRulesAsync(It.IsAny())) .Throws(new RuntimeException("test")); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitSearchExpression(searchExpression)); @@ -101,34 +116,42 @@ public async Task VisitSearchExpression_GivenSearchExpressionFailingRuntimeEvalu [Theory] [MemberData(nameof(ValidCasesSearchExpression))] public async Task VisitSearchExpression_GivenValidSearchExpressionForOneCardinality_ReturnsRqlArrayWithOneRule( - object contentTypeName, + object rulesetName, 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)) + var ruleResult = Rule.Create("Dummy rule") + .InRuleset(Rulesets.Type1) + .SetContent("test") + .Since(DateTime.Now) + .ApplyWhen(x => x.Value(Conditions.IsVip, Operators.Equal, false)) .Build(); - var conditions = hasConditions ? new[] { new Condition(ConditionType.IsVip, false) } : null; + var conditions = hasConditions + ? new Dictionary(StringComparer.Ordinal) + { + { nameof(Conditions.IsVip), false }, + } + : null; var expected = new RqlArray(1); - expected.SetAtIndex(0, new RqlRule(ruleResult.Rule)); + expected.SetAtIndex(0, new RqlRule(ruleResult.Rule)); - var contentTypeExpression = CreateMockedExpression((IRuntimeValue)contentTypeName); + var rulesetExpression = CreateMockedExpression((IRuntimeValue)rulesetName); 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 inputConditionsSegment = CreateMockedSegment(conditions!); + var searchExpression = new SearchExpression(rulesetExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); - var runtime = Mock.Of>(); + var runtime = Mock.Of(); + Mock.Get(runtime) + .Setup(x => x.GetRulesetsAsync()) + .ReturnsAsync(NewRqlArray(new RqlRuleset(new Ruleset("Type1", DateTime.UtcNow)))); Mock.Get(runtime) - .Setup(x => x.SearchRulesAsync(It.IsAny>())) + .Setup(x => x.SearchRulesAsync(It.IsAny())) .Returns(new ValueTask(expected)); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitSearchExpression(searchExpression); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs index 02b1e422..0e8d57f1 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs @@ -8,7 +8,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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 @@ -21,13 +20,13 @@ public async Task VisitUnaryExpression_GivenUnaryExpressionWithKnownOperator_App var targetExpression = CreateMockedExpression(NewRqlInteger(10)); var unaryExpression = new UnaryExpression(minusToken, targetExpression); - var runtime = Mock.Of>(); + 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); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.VisitUnaryExpression(unaryExpression); @@ -45,13 +44,13 @@ public async Task VisitUnaryExpression_GivenUnaryExpressionWithUnknownOperator_T var targetExpression = CreateMockedExpression(NewRqlInteger(10)); var unaryExpression = new UnaryExpression(minusToken, targetExpression); - var runtime = Mock.Of>(); + 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); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actualException = await Assert.ThrowsAsync(async () => await interpreter.VisitUnaryExpression(unaryExpression)); diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs index 254737e2..0d44b14a 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs @@ -11,7 +11,6 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret 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; @@ -29,10 +28,10 @@ public async Task InterpretAsync_GivenInvalidStatementThatIssuesAnError_ReturnsE .Throws(expectedException); var statements = new[] { mockStatementToExecute.Object }; - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.InterpretAsync(statements); @@ -56,10 +55,10 @@ public async Task InterpretAsync_GivenValidStatement_ExecutesAndReturnsResult() .Returns(Task.FromResult(expected)); var statements = new[] { mockStatementToExecute.Object }; - var runtime = Mock.Of>(); + var runtime = Mock.Of(); var reverseRqlBuilder = Mock.Of(); - var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); // Act var actual = await interpreter.InterpretAsync(statements); @@ -102,7 +101,7 @@ private static RqlAny NewRqlAny(IRuntimeValue runtimeValue) private static RqlArray NewRqlArray(params IRuntimeValue[] runtimeValues) { var rqlArray = new RqlArray(runtimeValues.Length); - for (int i = 0; i < runtimeValues.Length; i++) + for (var i = 0; i < runtimeValues.Length; i++) { rqlArray.SetAtIndex(i, NewRqlAny(runtimeValues[i])); } diff --git a/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs b/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs index da917e37..1568a851 100644 --- a/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs +++ b/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs @@ -304,8 +304,8 @@ public void VisitMatchExpression_GivenMatchExpressionWithConditions_ReturnsRqlRe Mock.Get(cardinalitySegment) .Setup(x => x.Accept(It.IsAny>())) .Returns("ONE RULE"); - var contentTypeExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); - Mock.Get(contentTypeExpression) + var rulesetExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(rulesetExpression) .Setup(x => x.Accept(It.IsAny>())) .Returns("\"Test\""); var matchDateExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); @@ -316,7 +316,7 @@ public void VisitMatchExpression_GivenMatchExpressionWithConditions_ReturnsRqlRe Mock.Get(inputConditionsSegment) .Setup(x => x.Accept(It.IsAny>())) .Returns("WITH { @TestCondition1 is true }"); - var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + var matchExpression = MatchExpression.Create(cardinalitySegment, rulesetExpression, matchDateExpression, inputConditionsSegment); var reverseRqlBuilder = new ReverseRqlBuilder(); @@ -335,8 +335,8 @@ public void VisitMatchExpression_GivenMatchExpressionWithoutConditions_ReturnsRq Mock.Get(cardinalitySegment) .Setup(x => x.Accept(It.IsAny>())) .Returns("ONE RULE"); - var contentTypeExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); - Mock.Get(contentTypeExpression) + var rulesetExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(rulesetExpression) .Setup(x => x.Accept(It.IsAny>())) .Returns("\"Test\""); var matchDateExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); @@ -347,7 +347,7 @@ public void VisitMatchExpression_GivenMatchExpressionWithoutConditions_ReturnsRq Mock.Get(inputConditionsSegment) .Setup(x => x.Accept(It.IsAny>())) .Returns(string.Empty); - var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + var matchExpression = MatchExpression.Create(cardinalitySegment, rulesetExpression, matchDateExpression, inputConditionsSegment); var reverseRqlBuilder = new ReverseRqlBuilder(); @@ -517,7 +517,7 @@ public void VisitOperatorSegment_GivenOperatorSegment_ReturnsRqlRepresentation(s { // Act var tokens = new Token[operatorTokens.Length]; - for (int i = 0; i < operatorTokens.Length; i++) + for (var i = 0; i < operatorTokens.Length; i++) { tokens[i] = Token.Create( operatorTokens[i], @@ -561,10 +561,10 @@ public void VisitPlaceholderExpression_GivenPlaceholderExpression_ReturnsRqlRepr public void VisitSearchExpression_GivenSearchExpressionWithInputConditions_ReturnsRqlRepresentation() { // Arrange - var contentType = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); - Mock.Get(contentType) + var ruleset = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(ruleset) .Setup(x => x.Accept(It.IsAny>())) - .Returns("\"test content type\""); + .Returns("\"test ruleset\""); var dateBegin = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); Mock.Get(dateBegin) @@ -581,7 +581,7 @@ public void VisitSearchExpression_GivenSearchExpressionWithInputConditions_Retur .Setup(x => x.Accept(It.IsAny>())) .Returns("WITH { @TestCondition1 is \"abc\" }"); - var searchExpression = new SearchExpression(contentType, dateBegin, dateEnd, inputConditions); + var searchExpression = new SearchExpression(ruleset, dateBegin, dateEnd, inputConditions); var reverseRqlBuilder = new ReverseRqlBuilder(); @@ -589,17 +589,17 @@ public void VisitSearchExpression_GivenSearchExpressionWithInputConditions_Retur 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\" }"); + actual.Should().Be("SEARCH RULES FOR \"test ruleset\" 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) + var ruleset = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(ruleset) .Setup(x => x.Accept(It.IsAny>())) - .Returns("\"test content type\""); + .Returns("\"test ruleset\""); var dateBegin = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); Mock.Get(dateBegin) @@ -616,7 +616,7 @@ public void VisitSearchExpression_GivenSearchExpressionWithoutInputConditions_Re .Setup(x => x.Accept(It.IsAny>())) .Returns(string.Empty); - var searchExpression = new SearchExpression(contentType, dateBegin, dateEnd, inputConditions); + var searchExpression = new SearchExpression(ruleset, dateBegin, dateEnd, inputConditions); var reverseRqlBuilder = new ReverseRqlBuilder(); @@ -624,7 +624,7 @@ public void VisitSearchExpression_GivenSearchExpressionWithoutInputConditions_Re var actual = reverseRqlBuilder.VisitSearchExpression(searchExpression); // Assert - actual.Should().Be("SEARCH RULES FOR \"test content type\" SINCE $2023-01-01$ UNTIL $2024-01-01$"); + actual.Should().Be("SEARCH RULES FOR \"test ruleset\" SINCE $2023-01-01$ UNTIL $2024-01-01$"); } [Fact] diff --git a/tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs b/tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs index d482f21a..68bad544 100644 --- a/tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs +++ b/tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs @@ -2,7 +2,6 @@ namespace Rules.Framework.Rql.Tests { using FluentAssertions; using Moq; - using Rules.Framework.Rql.Tests.Stubs; using Xunit; public class RqlEngineBuilderTests @@ -11,11 +10,11 @@ public class RqlEngineBuilderTests public void Build_GivenNullRqlOptions_ThrowsArgumentNullException() { // Arrange - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of(); // Act var argumentNullException = Assert.Throws(() => - RqlEngineBuilder.CreateRqlEngine(rulesEngine) + RqlEngineBuilder.CreateRqlEngine(rulesEngine) .WithOptions(null)); // Assert @@ -28,7 +27,7 @@ public void Build_GivenNullRulesEngine_ThrowsArgumentNullException() { // Act var argumentNullException = Assert.Throws(() => - RqlEngineBuilder.CreateRqlEngine(null)); + RqlEngineBuilder.CreateRqlEngine(null)); // Assert argumentNullException.Should().NotBeNull(); @@ -39,11 +38,11 @@ public void Build_GivenNullRulesEngine_ThrowsArgumentNullException() public void Build_GivenRulesEngineAndRqlOptions_BuildsRqlEngine() { // Arrange - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of(); var rqlOptions = RqlOptions.NewWithDefaults(); // Act - var rqlEngine = RqlEngineBuilder.CreateRqlEngine(rulesEngine) + var rqlEngine = RqlEngineBuilder.CreateRqlEngine(rulesEngine) .WithOptions(rqlOptions) .Build(); diff --git a/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs index 73c9336f..2da19e57 100644 --- a/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs +++ b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs @@ -12,7 +12,6 @@ namespace Rules.Framework.Rql.Tests 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; @@ -21,7 +20,7 @@ public class RqlEngineTests { private readonly IInterpreter interpreter; private readonly IParser parser; - private readonly RqlEngine rqlEngine; + private readonly RqlEngine rqlEngine; private readonly ITokenScanner tokenScanner; public RqlEngineTests() @@ -36,7 +35,7 @@ public RqlEngineTests() TokenScanner = tokenScanner, }; - this.rqlEngine = new RqlEngine(rqlEngineArgs); + this.rqlEngine = new RqlEngine(rqlEngineArgs); } [Fact] @@ -96,7 +95,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase1_InterpretsAndReturnsResult() Segment.None)), }.ToList().AsReadOnly(); var parseResult = ParseResult.CreateSuccess(statements, new List()); - var rqlRule = new RqlRule(); + var rqlRule = new RqlRule(); var rqlArray = new RqlArray(1); rqlArray.SetAtIndex(0, rqlRule); var interpretResult = new InterpretResult(); @@ -131,10 +130,10 @@ public async Task ExecuteAsync_GivenRqlSourceCase1_InterpretsAndReturnsResult() .Rql.Should().Be("MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$;"); var result2 = results.LastOrDefault(); result2.Should().NotBeNull() - .And.BeOfType>(); - result2.As>() + .And.BeOfType(); + result2.As() .Rql.Should().Be("MATCH ONE RULE FOR \"Other\\nTest\" ON $2024-01-01Z$;"); - result2.As>() + result2.As() .Lines.Should().HaveCount(1) .And.Contain(line => line.LineNumber == 1 && object.Equals(line.Rule, rqlRule)); } diff --git a/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs b/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs index ef708d06..0b57daff 100644 --- a/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs +++ b/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs @@ -2,55 +2,15 @@ namespace Rules.Framework.Rql.Tests { using FluentAssertions; using Moq; - using Rules.Framework.Rql.Tests.Stubs; using Xunit; public class RulesEngineExtensionsTests { [Fact] - public void GetRqlEngine_GivenRulesEngineWithEnumType_BuildsRqlEngineWithDefaultRqlOptions() + 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 only supported for enum types or strings on 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 only supported for enum types or strings on TContentType."); - } - - [Fact] - public void GetRqlEngine_GivenRulesEngineWithStringTypes_BuildsRqlEngineWithDefaultRqlOptions() - { - // Arrange - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of(); // Act var rqlEngine = rulesEngine.GetRqlEngine(); diff --git a/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs b/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs index 0ed37c1c..7fb286a7 100644 --- a/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs +++ b/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs @@ -2,7 +2,6 @@ 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; @@ -89,8 +88,8 @@ public class RqlRuntimeTests public void ApplyBinary_ErrorConditions_ThrowsRuntimeException(object left, object @operator, object right, string expectedErrorMessage) { // Arrange - var rulesEngine = Mock.Of>(); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rulesEngine = Mock.Of(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var runtimeException = Assert.Throws(() => rqlRuntime.ApplyBinary((IRuntimeValue)left, (RqlOperators)@operator, (IRuntimeValue)right)); @@ -104,8 +103,8 @@ public void ApplyBinary_ErrorConditions_ThrowsRuntimeException(object left, obje public void ApplyBinary_SuccessConditions_ReturnsBinaryResult(object left, object @operator, object right, object expected) { // Arrange - var rulesEngine = Mock.Of>(); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rulesEngine = Mock.Of(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var actual = rqlRuntime.ApplyBinary((IRuntimeValue)left, (RqlOperators)@operator, (IRuntimeValue)right); @@ -119,8 +118,8 @@ public void ApplyBinary_SuccessConditions_ReturnsBinaryResult(object left, objec public void ApplyUnary_ErrorConditions_ThrowsRuntimeException(object operand, object @operator, string expectedErrorMessage) { // Arrange - var rulesEngine = Mock.Of>(); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rulesEngine = Mock.Of(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var runtimeException = Assert.Throws(() => rqlRuntime.ApplyUnary((IRuntimeValue)operand, (RqlOperators)@operator)); @@ -134,8 +133,8 @@ public void ApplyUnary_ErrorConditions_ThrowsRuntimeException(object operand, ob public void ApplyUnary_SuccessConditions_ReturnsUnaryResult(object operand, object @operator, object expected) { // Arrange - var rulesEngine = Mock.Of>(); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rulesEngine = Mock.Of(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var actual = rqlRuntime.ApplyUnary((IRuntimeValue)operand, (RqlOperators)@operator); @@ -148,10 +147,10 @@ public void ApplyUnary_SuccessConditions_ReturnsUnaryResult(object operand, obje public void Create_GivenRulesEngine_ReturnsNewRqlRuntime() { // Arrange - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of(); // Act - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Assert rqlRuntime.Should().NotBeNull(); @@ -162,28 +161,28 @@ public async Task MatchRulesAsync_GivenAllMatchCardinalityWithResult_ReturnsRqlA { // Arrange const MatchCardinality matchCardinality = MatchCardinality.All; - const ContentType contentType = ContentType.Type1; + var ruleset = nameof(Rulesets.Type1); var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); - var conditions = new[] + var conditions = new Dictionary(StringComparer.Ordinal) { - new Condition(ConditionType.IsoCountryCode, "PT") + { nameof(Conditions.IsoCountryCode), "PT" }, }; - var matchRulesArgs = new MatchRulesArgs + var matchRulesArgs = new MatchRulesArgs { Conditions = conditions, - ContentType = contentType, + Ruleset = ruleset, 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 expectedRule1 = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), ruleset); + var expectedRule2 = BuildRule("Rule 2", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), ruleset); var expectedRules = new[] { expectedRule1, expectedRule2 }; - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of(); Mock.Get(rulesEngine) - .Setup(x => x.MatchManyAsync(contentType, matchDate.Value, It.Is>>(c => c.SequenceEqual(conditions)))) + .Setup(x => x.MatchManyAsync(ruleset, matchDate.Value, It.Is>(c => c.SequenceEqual(conditions)))) .ReturnsAsync(expectedRules); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var actual = await rqlRuntime.MatchRulesAsync(matchRulesArgs); @@ -191,9 +190,9 @@ public async Task MatchRulesAsync_GivenAllMatchCardinalityWithResult_ReturnsRqlA // Assert actual.Should().NotBeNull(); actual.Size.Value.Should().Be(2); - actual.Value[0].Unwrap().Should().BeOfType>() + actual.Value[0].Unwrap().Should().BeOfType() .Subject.Value.Should().BeSameAs(expectedRule1); - actual.Value[1].Unwrap().Should().BeOfType>() + actual.Value[1].Unwrap().Should().BeOfType() .Subject.Value.Should().BeSameAs(expectedRule2); } @@ -202,19 +201,19 @@ public async Task MatchRulesAsync_GivenNoneMatchCardinality_ThrowsArgumentExcept { // Arrange const MatchCardinality matchCardinality = MatchCardinality.None; - const ContentType contentType = ContentType.Type1; + var ruleset = nameof(Rulesets.Type1); var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); - var conditions = Array.Empty>(); - var matchRulesArgs = new MatchRulesArgs + var conditions = new Dictionary(StringComparer.Ordinal); + var matchRulesArgs = new MatchRulesArgs { Conditions = conditions, - ContentType = contentType, + Ruleset = ruleset, MatchCardinality = matchCardinality, MatchDate = matchDate, }; - var rulesEngine = Mock.Of>(); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rulesEngine = Mock.Of(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var actual = await Assert.ThrowsAsync(async () => await rqlRuntime.MatchRulesAsync(matchRulesArgs)); @@ -230,25 +229,25 @@ public async Task MatchRulesAsync_GivenOneMatchCardinalityWithoutResult_ReturnsE { // Arrange const MatchCardinality matchCardinality = MatchCardinality.One; - const ContentType contentType = ContentType.Type1; + var ruleset = nameof(Rulesets.Type1); var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); - var conditions = new[] + var conditions = new Dictionary(StringComparer.Ordinal) { - new Condition(ConditionType.IsoCountryCode, "PT") + { nameof(Conditions.IsoCountryCode), "PT" }, }; - var matchRulesArgs = new MatchRulesArgs + var matchRulesArgs = new MatchRulesArgs { Conditions = conditions, - ContentType = contentType, + Ruleset = ruleset, MatchCardinality = matchCardinality, MatchDate = matchDate, }; - var rulesEngine = Mock.Of>(); + 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); + .Setup(x => x.MatchOneAsync(ruleset, matchDate.Value, It.Is>(c => c.SequenceEqual(conditions)))) + .Returns(Task.FromResult(null!)); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var actual = await rqlRuntime.MatchRulesAsync(matchRulesArgs); @@ -263,26 +262,26 @@ public async Task MatchRulesAsync_GivenOneMatchCardinalityWithResult_ReturnsRqlA { // Arrange const MatchCardinality matchCardinality = MatchCardinality.One; - const ContentType contentType = ContentType.Type1; + var ruleset = nameof(Rulesets.Type1); var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); - var conditions = new[] + var conditions = new Dictionary(StringComparer.Ordinal) { - new Condition(ConditionType.IsoCountryCode, "PT") + { nameof(Conditions.IsoCountryCode), "PT" }, }; - var matchRulesArgs = new MatchRulesArgs + var matchRulesArgs = new MatchRulesArgs { Conditions = conditions, - ContentType = contentType, + Ruleset = ruleset, 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>(); + var expectedRule = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), ruleset); + var rulesEngine = Mock.Of(); Mock.Get(rulesEngine) - .Setup(x => x.MatchOneAsync(contentType, matchDate.Value, It.Is>>(c => c.SequenceEqual(conditions)))) + .Setup(x => x.MatchOneAsync(ruleset, matchDate.Value, It.Is>(c => c.SequenceEqual(conditions)))) .ReturnsAsync(expectedRule); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var actual = await rqlRuntime.MatchRulesAsync(matchRulesArgs); @@ -290,7 +289,7 @@ public async Task MatchRulesAsync_GivenOneMatchCardinalityWithResult_ReturnsRqlA // Assert actual.Should().NotBeNull(); actual.Size.Value.Should().Be(1); - actual.Value[0].Unwrap().Should().BeOfType>() + actual.Value[0].Unwrap().Should().BeOfType() .Subject.Value.Should().BeSameAs(expectedRule); } @@ -298,33 +297,33 @@ public async Task MatchRulesAsync_GivenOneMatchCardinalityWithResult_ReturnsRqlA public async Task MatchSearchRulesAsync_GivenSearchArgs_ReturnsRqlArrayWithTwoRules() { // Arrange - const ContentType contentType = ContentType.Type1; + var ruleset = nameof(Rulesets.Type1); var dateBegin = new RqlDate(DateTime.Parse("2020-01-01Z")); var dateEnd = new RqlDate(DateTime.Parse("2030-01-01Z")); - var conditions = new[] + var conditions = new Dictionary(StringComparer.Ordinal) { - new Condition(ConditionType.IsoCountryCode, "PT") + { nameof(Conditions.IsoCountryCode), "PT" }, }; - var searchRulesArgs = new SearchRulesArgs + var searchRulesArgs = new SearchRulesArgs { Conditions = conditions, - ContentType = contentType, + Ruleset = ruleset, 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 expectedRule1 = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), ruleset); + var expectedRule2 = BuildRule("Rule 2", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), ruleset); var expectedRules = new[] { expectedRule1, expectedRule2 }; - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of(); Mock.Get(rulesEngine) - .Setup(x => x.SearchAsync(It.Is>(c => c.ExcludeRulesWithoutSearchConditions == true + .Setup(x => x.SearchAsync(It.Is>(c => c.ExcludeRulesWithoutSearchConditions == true && c.Conditions.Equals(searchRulesArgs.Conditions) - && c.ContentType.Equals(searchRulesArgs.ContentType) + && c.Ruleset.Equals(searchRulesArgs.Ruleset) && c.DateBegin.Equals(searchRulesArgs.DateBegin.Value) && c.DateEnd.Equals(searchRulesArgs.DateEnd.Value)))) .ReturnsAsync(expectedRules); - var rqlRuntime = RqlRuntime.Create(rulesEngine); + var rqlRuntime = RqlRuntime.Create(rulesEngine); // Act var actual = await rqlRuntime.SearchRulesAsync(searchRulesArgs); @@ -332,19 +331,17 @@ public async Task MatchSearchRulesAsync_GivenSearchArgs_ReturnsRqlArrayWithTwoRu // Assert actual.Should().NotBeNull(); actual.Size.Value.Should().Be(2); - actual.Value[0].Unwrap().Should().BeOfType>() + actual.Value[0].Unwrap().Should().BeOfType() .Subject.Value.Should().BeSameAs(expectedRule1); - actual.Value[1].Unwrap().Should().BeOfType>() + 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) + private static Rule BuildRule(string name, DateTime dateBegin, DateTime? dateEnd, object content, string ruleset) + => Rule.Create(name) + .InRuleset(ruleset) + .SetContent(content) + .Since(dateBegin).Until(dateEnd) .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/Conditions.cs similarity index 85% rename from tests/Rules.Framework.Rql.Tests/TestStubs/ConditionType.cs rename to tests/Rules.Framework.Rql.Tests/TestStubs/Conditions.cs index 55a1a08b..8920b7c0 100644 --- a/tests/Rules.Framework.Rql.Tests/TestStubs/ConditionType.cs +++ b/tests/Rules.Framework.Rql.Tests/TestStubs/Conditions.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Rql.Tests.Stubs { - internal enum ConditionType + internal enum Conditions { IsoCountryCode = 1, diff --git a/tests/Rules.Framework.Rql.Tests/TestStubs/ContentType.cs b/tests/Rules.Framework.Rql.Tests/TestStubs/Rulesets.cs similarity index 76% rename from tests/Rules.Framework.Rql.Tests/TestStubs/ContentType.cs rename to tests/Rules.Framework.Rql.Tests/TestStubs/Rulesets.cs index 6bb08462..20bc47f8 100644 --- a/tests/Rules.Framework.Rql.Tests/TestStubs/ContentType.cs +++ b/tests/Rules.Framework.Rql.Tests/TestStubs/Rulesets.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Rql.Tests.Stubs { - internal enum ContentType + internal enum Rulesets { Type1 = 1, diff --git a/tests/Rules.Framework.RqlReplTester/Program.cs b/tests/Rules.Framework.RqlReplTester/Program.cs index 216f4fe5..34b91727 100644 --- a/tests/Rules.Framework.RqlReplTester/Program.cs +++ b/tests/Rules.Framework.RqlReplTester/Program.cs @@ -23,7 +23,7 @@ private static async Task ExecuteAsync(IRqlEngine rqlEngine, string? input) Console.ForegroundColor = originalConsoleForegroundColor; switch (result) { - case RulesSetResult rulesResultSet: + case RulesSetResult rulesResultSet: HandleRulesSetResult(rulesResultSet); break; @@ -75,7 +75,7 @@ private static void HandleObjectResult(ValueResult result) Console.WriteLine($"{tab}{value}"); } - private static void HandleRulesSetResult(RulesSetResult result) + private static void HandleRulesSetResult(RulesSetResult result) { Console.WriteLine(); if (result.Lines.Any()) @@ -147,10 +147,8 @@ private static async Task Main(string[] args) app.OnExecuteAsync(async (ct) => { var rulesEngine = RulesEngineBuilder.CreateRulesEngine() - .WithContentType() - .WithConditionType() - .SetInMemoryDataSource() - .Build(); + .SetInMemoryDataSource() + .Build(); await ScenarioLoader.LoadScenarioAsync(rulesEngine, new Scenario8Data()); var rqlEngine = rulesEngine.GetRqlEngine(); diff --git a/tests/Rules.Framework.Tests/Builder/RuleBuilderTests.cs b/tests/Rules.Framework.Tests/Builder/RuleBuilderTests.cs index a15dc2bd..617a89f8 100644 --- a/tests/Rules.Framework.Tests/Builder/RuleBuilderTests.cs +++ b/tests/Rules.Framework.Tests/Builder/RuleBuilderTests.cs @@ -85,14 +85,14 @@ public void NewRule_GivenRuleWithComposedCondition_BuildsAndReturnsRule() [Theory] [InlineData(Operators.Contains)] [InlineData(Operators.NotContains)] - public void NewRule_GivenRuleWithIntegerConditionTypeAndContainsOperator_ReturnsInvalidRuleResult(Operators containsOperator) + public void NewRule_GivenRuleWithIntegerConditionAndContainsOperator_ReturnsInvalidRuleResult(Operators containsOperator) { // Arrange var ruleName = "Rule 1"; var dateBegin = DateTime.Parse("2021-01-01"); var ruleset = RulesetNames.Type1; var content = "Content"; - const ConditionNames conditionType = ConditionNames.NumberOfSales; + const ConditionNames condition = ConditionNames.NumberOfSales; const int conditionValue = 40; var conditionOperator = containsOperator; const DataTypes dataType = DataTypes.Integer; @@ -102,7 +102,7 @@ public void NewRule_GivenRuleWithIntegerConditionTypeAndContainsOperator_Returns .InRuleset(ruleset) .SetContent(content) .Since(dateBegin) - .ApplyWhen(conditionType, conditionOperator, conditionValue) + .ApplyWhen(condition, conditionOperator, conditionValue) .Build(); // Assert @@ -117,14 +117,14 @@ public void NewRule_GivenRuleWithIntegerConditionTypeAndContainsOperator_Returns [Theory] [InlineData(Operators.Contains)] [InlineData(Operators.NotContains)] - public void NewRule_GivenRuleWithStringConditionTypeAndContainsOperator_BuildsAndReturnsRule(Operators containsOperator) + public void NewRule_GivenRuleWithStringConditionAndContainsOperator_BuildsAndReturnsRule(Operators containsOperator) { // Arrange var ruleName = "Rule 1"; var dateBegin = DateTime.Parse("2021-01-01"); var ruleset = RulesetNames.Type1; var content = "Content"; - const ConditionNames conditionType = ConditionNames.IsoCountryCode; + const ConditionNames condition = ConditionNames.IsoCountryCode; const string conditionValue = "PT"; var conditionOperator = containsOperator; const LogicalOperators logicalOperator = LogicalOperators.Eval; @@ -135,7 +135,7 @@ public void NewRule_GivenRuleWithStringConditionTypeAndContainsOperator_BuildsAn .InRuleset(ruleset) .SetContent(content) .Since(dateBegin) - .ApplyWhen(c => c.Value(conditionType, conditionOperator, conditionValue)) + .ApplyWhen(c => c.Value(condition, conditionOperator, conditionValue)) .Build(); // Assert @@ -153,7 +153,7 @@ public void NewRule_GivenRuleWithStringConditionTypeAndContainsOperator_BuildsAn rule.RootCondition.Should().BeAssignableTo>(); var rootCondition = rule.RootCondition as IValueConditionNode; - rootCondition.Condition.Should().Be(conditionType); + rootCondition.Condition.Should().Be(condition); rootCondition.DataType.Should().Be(dataType); rootCondition.LogicalOperator.Should().Be(logicalOperator); rootCondition.Operator.Should().Be(conditionOperator); diff --git a/tests/Rules.Framework.Tests/ConditionNodes/ValueConditionNodeTests.cs b/tests/Rules.Framework.Tests/ConditionNodes/ValueConditionNodeTests.cs index cf666701..1b120ad5 100644 --- a/tests/Rules.Framework.Tests/ConditionNodes/ValueConditionNodeTests.cs +++ b/tests/Rules.Framework.Tests/ConditionNodes/ValueConditionNodeTests.cs @@ -11,13 +11,13 @@ public class ValueConditionNodeTests public void Clone_BooleanDataType_ReturnsCloneInstance() { // Arrange - var expectedConditionType = ConditionNames.IsoCountryCode.ToString(); + var expectedCondition = ConditionNames.IsoCountryCode.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = false; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.Boolean; - var sut = new ValueConditionNode(DataTypes.Boolean, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(DataTypes.Boolean, expectedCondition, expectedOperator, expectedOperand); sut.Properties["test"] = "test"; // Act @@ -29,7 +29,7 @@ public void Clone_BooleanDataType_ReturnsCloneInstance() .And .BeOfType(); var valueConditionNode = actual.As(); - valueConditionNode.Condition.Should().Be(expectedConditionType); + valueConditionNode.Condition.Should().Be(expectedCondition); valueConditionNode.DataType.Should().Be(expectedDataType); valueConditionNode.LogicalOperator.Should().Be(expectedLogicalOperator); valueConditionNode.Operator.Should().Be(expectedOperator); @@ -41,13 +41,13 @@ public void Clone_BooleanDataType_ReturnsCloneInstance() public void Clone_DecimalDataType_ReturnsCloneInstance() { // Arrange - var expectedConditionType = ConditionNames.PluviosityRate.ToString(); + var expectedCondition = ConditionNames.PluviosityRate.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = 5682.2654m; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.Decimal; - var sut = new ValueConditionNode(expectedDataType, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(expectedDataType, expectedCondition, expectedOperator, expectedOperand); sut.Properties["test"] = "test"; // Act @@ -59,7 +59,7 @@ public void Clone_DecimalDataType_ReturnsCloneInstance() .And .BeOfType(); var valueConditionNode = actual.As(); - valueConditionNode.Condition.Should().Be(expectedConditionType); + valueConditionNode.Condition.Should().Be(expectedCondition); valueConditionNode.DataType.Should().Be(expectedDataType); valueConditionNode.LogicalOperator.Should().Be(expectedLogicalOperator); valueConditionNode.Operator.Should().Be(expectedOperator); @@ -71,13 +71,13 @@ public void Clone_DecimalDataType_ReturnsCloneInstance() public void Clone_IntegerDataType_ReturnsCloneInstance() { // Arrange - var expectedConditionType = ConditionNames.IsoCountryCode.ToString(); + var expectedCondition = ConditionNames.IsoCountryCode.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = 1616; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.Integer; - var sut = new ValueConditionNode(expectedDataType, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(expectedDataType, expectedCondition, expectedOperator, expectedOperand); sut.Properties["test"] = "test"; // Act @@ -89,7 +89,7 @@ public void Clone_IntegerDataType_ReturnsCloneInstance() .And .BeOfType(); var valueConditionNode = actual.As(); - valueConditionNode.Condition.Should().Be(expectedConditionType); + valueConditionNode.Condition.Should().Be(expectedCondition); valueConditionNode.DataType.Should().Be(expectedDataType); valueConditionNode.LogicalOperator.Should().Be(expectedLogicalOperator); valueConditionNode.Operator.Should().Be(expectedOperator); @@ -101,13 +101,13 @@ public void Clone_IntegerDataType_ReturnsCloneInstance() public void Clone_StringDataType_ReturnsCloneInstance() { // Arrange - var expectedConditionType = ConditionNames.IsoCountryCode.ToString(); + var expectedCondition = ConditionNames.IsoCountryCode.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = "Such operand, much wow."; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.String; - var sut = new ValueConditionNode(expectedDataType, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(expectedDataType, expectedCondition, expectedOperator, expectedOperand); sut.Properties["test"] = "test"; // Act @@ -119,7 +119,7 @@ public void Clone_StringDataType_ReturnsCloneInstance() .And .BeOfType(); var valueConditionNode = actual.As(); - valueConditionNode.Condition.Should().Be(expectedConditionType); + valueConditionNode.Condition.Should().Be(expectedCondition); valueConditionNode.DataType.Should().Be(expectedDataType); valueConditionNode.LogicalOperator.Should().Be(expectedLogicalOperator); valueConditionNode.Operator.Should().Be(expectedOperator); @@ -131,23 +131,23 @@ public void Clone_StringDataType_ReturnsCloneInstance() public void Init_GivenSetupWithBooleanValue_ReturnsSettedValues() { // Arrange - var expectedConditionType = ConditionNames.IsoCountryCode.ToString(); + var expectedCondition = ConditionNames.IsoCountryCode.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = false; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.Boolean; - var sut = new ValueConditionNode(expectedDataType, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(expectedDataType, expectedCondition, expectedOperator, expectedOperand); // Act - var actualConditionType = sut.Condition; + var actualCondition = sut.Condition; var actualOperator = sut.Operator; var actualDataType = sut.DataType; var actualLogicalOperator = sut.LogicalOperator; var actualOperand = sut.Operand; // Assert - actualConditionType.Should().Be(expectedConditionType); + actualCondition.Should().Be(expectedCondition); actualOperator.Should().Be(expectedOperator); actualOperand.Should().Be(expectedOperand); actualLogicalOperator.Should().Be(expectedLogicalOperator); @@ -158,23 +158,23 @@ public void Init_GivenSetupWithBooleanValue_ReturnsSettedValues() public void Init_GivenSetupWithDecimalValue_ReturnsSettedValues() { // Arrange - var expectedConditionType = ConditionNames.PluviosityRate.ToString(); + var expectedCondition = ConditionNames.PluviosityRate.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = 5682.2654m; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.Decimal; - var sut = new ValueConditionNode(DataTypes.Decimal, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(DataTypes.Decimal, expectedCondition, expectedOperator, expectedOperand); // Act - var actualConditionType = sut.Condition; + var actualCondition = sut.Condition; var actualOperator = sut.Operator; var actualDataType = sut.DataType; var actualLogicalOperator = sut.LogicalOperator; var actualOperand = sut.Operand; // Assert - actualConditionType.Should().Be(expectedConditionType); + actualCondition.Should().Be(expectedCondition); actualOperator.Should().Be(expectedOperator); actualOperand.Should().Be(expectedOperand); actualLogicalOperator.Should().Be(expectedLogicalOperator); @@ -185,23 +185,23 @@ public void Init_GivenSetupWithDecimalValue_ReturnsSettedValues() public void Init_GivenSetupWithIntegerValue_ReturnsSettedValues() { // Arrange - var expectedConditionType = ConditionNames.IsoCountryCode.ToString(); + var expectedCondition = ConditionNames.IsoCountryCode.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = 1616; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.Integer; - var sut = new ValueConditionNode(expectedDataType, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(expectedDataType, expectedCondition, expectedOperator, expectedOperand); // Act - var actualConditionType = sut.Condition; + var actualCondition = sut.Condition; var actualOperator = sut.Operator; var actualDataType = sut.DataType; var actualLogicalOperator = sut.LogicalOperator; var actualOperand = sut.Operand; // Assert - actualConditionType.Should().Be(expectedConditionType); + actualCondition.Should().Be(expectedCondition); actualOperator.Should().Be(expectedOperator); actualOperand.Should().Be(expectedOperand); actualLogicalOperator.Should().Be(expectedLogicalOperator); @@ -212,23 +212,23 @@ public void Init_GivenSetupWithIntegerValue_ReturnsSettedValues() public void Init_GivenSetupWithStringValue_ReturnsSettedValues() { // Arrange - var expectedConditionType = ConditionNames.IsoCountryCode.ToString(); + var expectedCondition = ConditionNames.IsoCountryCode.ToString(); var expectedOperator = Operators.NotEqual; var expectedOperand = "Such operand, much wow."; var expectedLogicalOperator = LogicalOperators.Eval; var expectedDataType = DataTypes.String; - var sut = new ValueConditionNode(expectedDataType, expectedConditionType, expectedOperator, expectedOperand); + var sut = new ValueConditionNode(expectedDataType, expectedCondition, expectedOperator, expectedOperand); // Act - var actualConditionType = sut.Condition; + var actualCondition = sut.Condition; var actualOperator = sut.Operator; var actualDataType = sut.DataType; var actualLogicalOperator = sut.LogicalOperator; var actualOperand = sut.Operand; // Assert - actualConditionType.Should().Be(expectedConditionType); + actualCondition.Should().Be(expectedCondition); actualOperator.Should().Be(expectedOperator); actualOperand.Should().Be(expectedOperand); actualLogicalOperator.Should().Be(expectedLogicalOperator); diff --git a/tests/Rules.Framework.Tests/Evaluation/Compiled/CompilationRulesSourceMiddlewareTests.cs b/tests/Rules.Framework.Tests/Evaluation/Compiled/CompilationRulesSourceMiddlewareTests.cs index a0356680..ab3bc74e 100644 --- a/tests/Rules.Framework.Tests/Evaluation/Compiled/CompilationRulesSourceMiddlewareTests.cs +++ b/tests/Rules.Framework.Tests/Evaluation/Compiled/CompilationRulesSourceMiddlewareTests.cs @@ -164,7 +164,7 @@ public async Task HandleGetRulesAsync_GivenArgsFilteringToRulesWithCompiledCondi var getRulesArgs = new GetRulesArgs { - ContentType = RulesetNames.Type1.ToString(), + Ruleset = RulesetNames.Type1.ToString(), DateBegin = DateTime.UtcNow.AddDays(-1), DateEnd = DateTime.UtcNow.AddDays(1), }; @@ -214,7 +214,7 @@ public async Task HandleGetRulesAsync_GivenArgsFilteringToRulesWithoutConditions var getRulesArgs = new GetRulesArgs { - ContentType = RulesetNames.Type1.ToString(), + Ruleset = RulesetNames.Type1.ToString(), DateBegin = DateTime.UtcNow.AddDays(-1), DateEnd = DateTime.UtcNow.AddDays(1), }; @@ -264,7 +264,7 @@ public async Task HandleGetRulesAsync_GivenArgsFilteringToRulesWithUncompiledCon var getRulesArgs = new GetRulesArgs { - ContentType = RulesetNames.Type1.ToString(), + Ruleset = RulesetNames.Type1.ToString(), DateBegin = DateTime.UtcNow.AddDays(-1), DateEnd = DateTime.UtcNow.AddDays(1), }; diff --git a/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionsValueLookupExtensionTests.cs b/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionsValueLookupExtensionTests.cs index e1943576..2b270f53 100644 --- a/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionsValueLookupExtensionTests.cs +++ b/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionsValueLookupExtensionTests.cs @@ -9,7 +9,7 @@ namespace Rules.Framework.Tests.Evaluation.Compiled public class ConditionsValueLookupExtensionTests { [Fact] - public void GetValueOrDefault_GivenConditionsDictionaryAndConditionType_ReturnsNull() + public void GetValueOrDefault_GivenConditionsDictionaryAndCondition_ReturnsNull() { // Arrange const string expected = "EUR"; @@ -17,24 +17,24 @@ public void GetValueOrDefault_GivenConditionsDictionaryAndConditionType_ReturnsN { { ConditionNames.IsoCurrency.ToString(), expected } }; - var conditionType = ConditionNames.IsoCurrency.ToString(); + var condition = ConditionNames.IsoCurrency.ToString(); // Act - var result = ConditionsValueLookupExtension.GetValueOrDefault(conditions, conditionType); + var result = ConditionsValueLookupExtension.GetValueOrDefault(conditions, condition); // Assert result.Should().Be(expected); } [Fact] - public void GetValueOrDefault_GivenEmptyConditionsDictionaryAndConditionType_ReturnsNull() + public void GetValueOrDefault_GivenEmptyConditionsDictionaryAndCondition_ReturnsNull() { // Arrange var conditions = new Dictionary(); - var conditionType = ConditionNames.IsoCurrency.ToString(); + var condition = ConditionNames.IsoCurrency.ToString(); // Act - var result = ConditionsValueLookupExtension.GetValueOrDefault(conditions, conditionType); + var result = ConditionsValueLookupExtension.GetValueOrDefault(conditions, condition); // Assert result.Should().BeNull(); diff --git a/tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs b/tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs index a66b7cc8..c81d842d 100644 --- a/tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs +++ b/tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs @@ -121,7 +121,7 @@ public void GenericRuleExtensions_ToGenericRule_WithValueCondition_Success() var expectedRuleContent = "Type1"; var expectedRootCondition = new { - ConditionType = ConditionNames.NumberOfSales, + Condition = ConditionNames.NumberOfSales, DataType = DataTypes.Integer, LogicalOperator = LogicalOperators.Eval, Operator = Operators.GreaterThan, @@ -151,7 +151,7 @@ public void GenericRuleExtensions_ToGenericRule_WithValueCondition_Success() genericRule.RootCondition.Should().BeOfType>(); var genericValueRootCondition = genericRule.RootCondition as ValueConditionNode; - genericValueRootCondition.Condition.Should().Be(expectedRootCondition.ConditionType); + genericValueRootCondition.Condition.Should().Be(expectedRootCondition.Condition); 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/Generic/RulesEngineTests.cs b/tests/Rules.Framework.Tests/Generic/RulesEngineTests.cs index 77ca5bce..5cba4693 100644 --- a/tests/Rules.Framework.Tests/Generic/RulesEngineTests.cs +++ b/tests/Rules.Framework.Tests/Generic/RulesEngineTests.cs @@ -49,9 +49,9 @@ public async Task GetRulesetsAsync_NoConditionsGiven_ReturnsRulesets() public async Task GetRulesetsAsync_WithEmptyRulesetsNames_ReturnsEmptyRulesetsCollection() { // Arrange - var mockRulesEngineEmptyContentType = new Mock(); + var mockRulesEngineEmptyRuleset = new Mock(); - var genericRulesEngine = new RulesEngine(mockRulesEngineEmptyContentType.Object); + var genericRulesEngine = new RulesEngine(mockRulesEngineEmptyRuleset.Object); // Act var genericRulesets = await genericRulesEngine.GetRulesetsAsync(); @@ -98,7 +98,7 @@ public void Options_PropertyGet_ReturnsRulesEngineOptions() } [Fact] - public async Task SearchAsync_GivenContentTypeAndDatesIntervalAndNoConditions_ReturnsRules() + public async Task SearchAsync_GivenRulesetAndDatesIntervalAndNoConditions_ReturnsRules() { // Arrange var expectedRule = Rule.Create("Test rule") @@ -112,9 +112,9 @@ public async Task SearchAsync_GivenContentTypeAndDatesIntervalAndNoConditions_Re var dateBegin = new DateTime(2022, 01, 01); var dateEnd = new DateTime(2022, 12, 01); - var genericContentType = RulesetNames.Type1; + var genericRuleset = RulesetNames.Type1; - var genericSearchArgs = new SearchArgs(genericContentType, dateBegin, dateEnd); + var genericSearchArgs = new SearchArgs(genericRuleset, dateBegin, dateEnd); var testRule = Rule.Create("Test rule") .InRuleset(RulesetNames.Type1) diff --git a/tests/Rules.Framework.Tests/Providers/InMemory/RuleFactoryTests.cs b/tests/Rules.Framework.Tests/Providers/InMemory/RuleFactoryTests.cs index ad83f23b..35591172 100644 --- a/tests/Rules.Framework.Tests/Providers/InMemory/RuleFactoryTests.cs +++ b/tests/Rules.Framework.Tests/Providers/InMemory/RuleFactoryTests.cs @@ -99,7 +99,7 @@ public void CreateRule_GivenRuleDataModelWithComposedNodeAndChildNodesOfEachData var ruleDataModel = new RuleDataModel { Content = content, - Ruleset = RulesetNames.ContentTypeSample.ToString(), + Ruleset = RulesetNames.RulesetSample.ToString(), DateBegin = new System.DateTime(2020, 1, 1), DateEnd = null, Name = "My rule used for testing purposes", @@ -192,7 +192,7 @@ public void CreateRule_GivenRuleWithComposedNodeAndChildNodesOfEachDataType_Retu .CreateValueNode(ConditionNames.SampleStringCondition.ToString(), Operators.Equal, "TEST") as ValueConditionNode; var rule1 = Rule.Create("My rule used for testing purposes") - .InRuleset(RulesetNames.ContentTypeSample) + .InRuleset(RulesetNames.RulesetSample) .SetContent((object)content) .Since(new DateTime(2020, 1, 1)) .ApplyWhen(c => c diff --git a/tests/Rules.Framework.Tests/Providers/InMemory/TestStubs/RulesetNames.cs b/tests/Rules.Framework.Tests/Providers/InMemory/TestStubs/RulesetNames.cs index d661a602..6d8ce82f 100644 --- a/tests/Rules.Framework.Tests/Providers/InMemory/TestStubs/RulesetNames.cs +++ b/tests/Rules.Framework.Tests/Providers/InMemory/TestStubs/RulesetNames.cs @@ -2,6 +2,6 @@ namespace Rules.Framework.Tests.Providers.InMemory.TestStubs { internal enum RulesetNames { - ContentTypeSample = 1 + RulesetSample = 1 } } \ No newline at end of file diff --git a/tests/Rules.Framework.Tests/RuleConditionsExtractorTests.cs b/tests/Rules.Framework.Tests/RuleConditionsExtractorTests.cs index ef392a11..1efd6d9f 100644 --- a/tests/Rules.Framework.Tests/RuleConditionsExtractorTests.cs +++ b/tests/Rules.Framework.Tests/RuleConditionsExtractorTests.cs @@ -11,7 +11,7 @@ namespace Rules.Framework.Tests public class RuleConditionsExtractorTests { [Fact] - public void GetConditionTypes_ReturnsCorrectExtraction() + public void GetConditions_ReturnsCorrectExtraction() { // Arrange @@ -74,7 +74,7 @@ public void GetConditionTypes_ReturnsCorrectExtraction() rule4 }; - var expectedConditionTypeList = new List + var expectedConditionList = new List { ConditionNames.IsoCurrency.ToString(), ConditionNames.IsoCountryCode.ToString(), @@ -82,35 +82,35 @@ public void GetConditionTypes_ReturnsCorrectExtraction() ConditionNames.PluviosityRate.ToString(), }; - var conditionTypeExtractor = new RuleConditionsExtractor(); + var ruleConditionsExtractor = new RuleConditionsExtractor(); // Act - var actual = conditionTypeExtractor.GetConditions(matchRules); + var actual = ruleConditionsExtractor.GetConditions(matchRules); // Assert - actual.Should().BeEquivalentTo(expectedConditionTypeList); + actual.Should().BeEquivalentTo(expectedConditionList); } [Fact] - public void GetConditionTypes_WithEmptyMatchRules_ReturnsEmptyListConditionTypes() + public void GetConditions_WithEmptyMatchRules_ReturnsEmptyListConditions() { // Arrange var matchRules = new List(); - var expectedConditionTypeList = new List(); + var expectedConditionList = new List(); - var conditionTypeExtractor = new RuleConditionsExtractor(); + var ruleConditionsExtractor = new RuleConditionsExtractor(); // Act - var actual = conditionTypeExtractor.GetConditions(matchRules); + var actual = ruleConditionsExtractor.GetConditions(matchRules); // Assert - actual.Should().BeEquivalentTo(expectedConditionTypeList); + actual.Should().BeEquivalentTo(expectedConditionList); } [Fact] - public void GetConditionTypes_WithNullRootCondition_ReturnsEmptyListConditionTypes() + public void GetConditions_WithNullRootCondition_ReturnsEmptyListConditions() { // Arrange @@ -130,15 +130,15 @@ public void GetConditionTypes_WithNullRootCondition_ReturnsEmptyListConditionTyp } }; - var expectedConditionTypeList = new List(); + var expectedConditionList = new List(); - var conditionTypeExtractor = new RuleConditionsExtractor(); + var ruleConditionsExtractor = new RuleConditionsExtractor(); // Act - var actual = conditionTypeExtractor.GetConditions(matchRules); + var actual = ruleConditionsExtractor.GetConditions(matchRules); // Assert - actual.Should().BeEquivalentTo(expectedConditionTypeList); + actual.Should().BeEquivalentTo(expectedConditionList); } } } \ No newline at end of file diff --git a/tests/Rules.Framework.Tests/RulesEngineTests.cs b/tests/Rules.Framework.Tests/RulesEngineTests.cs index af9c4500..0236fb31 100644 --- a/tests/Rules.Framework.Tests/RulesEngineTests.cs +++ b/tests/Rules.Framework.Tests/RulesEngineTests.cs @@ -21,14 +21,14 @@ namespace Rules.Framework.Tests public class RulesEngineTests { private readonly IConditionsEvalEngine conditionsEvalEngineMock; - private readonly IRuleConditionsExtractor conditionTypeExtractorMock; + private readonly IRuleConditionsExtractor ruleConditionsExtractorMock; private readonly IRulesSource rulesSourceMock; private readonly IValidatorProvider validatorProviderMock; public RulesEngineTests() { this.rulesSourceMock = Mock.Of(); - this.conditionTypeExtractorMock = Mock.Of(); + this.ruleConditionsExtractorMock = Mock.Of(); this.conditionsEvalEngineMock = Mock.Of(); this.validatorProviderMock = Mock.Of(); } @@ -58,7 +58,7 @@ public async Task ActivateRuleAsync_GivenEmptyRuleDataSource_ActivatesRuleSucces var validatorProvider = Mock.Of(); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.ActivateRuleAsync(testRule); @@ -96,7 +96,7 @@ public async Task AddRuleAsync_GivenEmptyRuleDataSourceAndExistentRuleset_AddsRu rulesEngineOptions.PriorityCriteria = PriorityCriterias.BottommostRuleWins; - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.AddRuleAsync(testRule, RuleAddPriorityOption.AtBottom); @@ -134,7 +134,7 @@ public async Task AddRuleAsync_GivenEmptyRuleDataSourceAndNonExistentRulesetAndA rulesEngineOptions.PriorityCriteria = PriorityCriterias.BottommostRuleWins; - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.AddRuleAsync(testRule, RuleAddPriorityOption.AtBottom); @@ -175,7 +175,7 @@ public async Task AddRuleAsync_GivenEmptyRuleDataSourceAndNonExistentRulesetAndA rulesEngineOptions.PriorityCriteria = PriorityCriterias.BottommostRuleWins; rulesEngineOptions.AutoCreateRulesets = true; - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.AddRuleAsync(testRule, RuleAddPriorityOption.AtBottom); @@ -194,7 +194,7 @@ public async Task AddRuleAsync_GivenEmptyRuleDataSourceAndNonExistentRulesetAndA public async Task CreateRulesetAsync_GivenExistentRulesetName_DoesNotAddRulesetToRulesSource() { // Arrange - var contentType = RulesetNames.Type1.ToString(); + var ruleset = RulesetNames.Type1.ToString(); Mock.Get(this.rulesSourceMock) .Setup(x => x.GetRulesetsAsync(It.IsAny())) @@ -204,10 +204,10 @@ public async Task CreateRulesetAsync_GivenExistentRulesetName_DoesNotAddRulesetT rulesEngineOptions.PriorityCriteria = PriorityCriterias.BottommostRuleWins; - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, ruleConditionsExtractorMock); // Act - var operationResult = await sut.CreateRulesetAsync(contentType); + var operationResult = await sut.CreateRulesetAsync(ruleset); // Assert operationResult.Should().NotBeNull(); @@ -216,7 +216,7 @@ public async Task CreateRulesetAsync_GivenExistentRulesetName_DoesNotAddRulesetT .And.HaveCount(1); Mock.Get(rulesSourceMock).Verify(x => x.GetRulesetsAsync(It.IsAny()), Times.Once()); - Mock.Get(rulesSourceMock).Verify(x => x.CreateRulesetAsync(It.Is(x => string.Equals(x.Name, contentType))), Times.Never()); + Mock.Get(rulesSourceMock).Verify(x => x.CreateRulesetAsync(It.Is(x => string.Equals(x.Name, ruleset))), Times.Never()); Mock.Get(conditionsEvalEngineMock).VerifyNoOtherCalls(); } @@ -224,23 +224,23 @@ public async Task CreateRulesetAsync_GivenExistentRulesetName_DoesNotAddRulesetT public async Task CreateRulesetAsync_GivenNonExistentRulesetName_AddsRulesetToRulesSource() { // Arrange - var contentType = RulesetNames.Type1.ToString(); + var ruleset = RulesetNames.Type1.ToString(); Mock.Get(this.rulesSourceMock) .Setup(x => x.GetRulesetsAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); Mock.Get(rulesSourceMock) - .Setup(x => x.CreateRulesetAsync(It.Is(x => string.Equals(x.Name, contentType)))) + .Setup(x => x.CreateRulesetAsync(It.Is(x => string.Equals(x.Name, ruleset)))) .Returns(Task.CompletedTask); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); rulesEngineOptions.PriorityCriteria = PriorityCriterias.BottommostRuleWins; - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, ruleConditionsExtractorMock); // Act - var operationResult = await sut.CreateRulesetAsync(contentType); + var operationResult = await sut.CreateRulesetAsync(ruleset); // Assert operationResult.Should().NotBeNull(); @@ -249,7 +249,7 @@ public async Task CreateRulesetAsync_GivenNonExistentRulesetName_AddsRulesetToRu .And.BeEmpty(); Mock.Get(rulesSourceMock).Verify(x => x.GetRulesetsAsync(It.IsAny()), Times.Once()); - Mock.Get(rulesSourceMock).Verify(x => x.CreateRulesetAsync(It.Is(x => string.Equals(x.Name, contentType))), Times.Once()); + Mock.Get(rulesSourceMock).Verify(x => x.CreateRulesetAsync(It.Is(x => string.Equals(x.Name, ruleset))), Times.Once()); Mock.Get(conditionsEvalEngineMock).VerifyNoOtherCalls(); } @@ -281,7 +281,7 @@ public async Task DeactivateRuleAsync_GivenEmptyRuleDataSource_DeactivatesRuleSu var validatorProvider = Mock.Of(); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.DeactivateRuleAsync(testRule); @@ -308,13 +308,13 @@ public async Task GetRulesetsAsync_NoConditionsGiven_ReturnsRulesetsFromRulesSou rulesEngineOptions.PriorityCriteria = PriorityCriterias.BottommostRuleWins; - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProviderMock, rulesEngineOptions, ruleConditionsExtractorMock); // Act - var contentTypes = await sut.GetRulesetsAsync(); + var rulesets = await sut.GetRulesetsAsync(); // Assert - contentTypes.Should().NotBeNull() + rulesets.Should().NotBeNull() .And.HaveCount(2) .And.Contain(r => string.Equals(r.Name, nameof(RulesetNames.Type1), StringComparison.Ordinal)) .And.Contain(r => string.Equals(r.Name, nameof(RulesetNames.Type2), StringComparison.Ordinal)); @@ -338,7 +338,7 @@ public async Task GetUniqueConditionsAsync_GivenThereAreRulesInDataSource_Return var expectedConditions = new List { ConditionNames.IsoCountryCode.ToString() }; - Mock.Get(conditionTypeExtractorMock) + Mock.Get(ruleConditionsExtractorMock) .Setup(x => x.GetConditions(It.IsAny>())) .Returns(expectedConditions); @@ -350,7 +350,7 @@ public async Task GetUniqueConditionsAsync_GivenThereAreRulesInDataSource_Return var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.GetUniqueConditionsAsync(RulesetNames.Type1.ToString(), dateBegin, dateEnd); @@ -425,7 +425,7 @@ public async Task MatchManyAsync_GivenRulesetDateAndConditions_FetchesRulesForDa var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.MatchManyAsync(ruleset, matchDateTime, conditions); @@ -493,7 +493,7 @@ public async Task MatchOneAsync_GivenRulesetDateAndConditions_FetchesRulesForDay rulesEngineOptions.PriorityCriteria = PriorityCriterias.BottommostRuleWins; - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.MatchOneAsync(ruleset, matchDateTime, conditions); @@ -557,7 +557,7 @@ public async Task MatchOneAsync_GivenRulesetDateAndConditions_FetchesRulesForDay var validatorProvider = Mock.Of(); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.MatchOneAsync(ruleset, matchDateTime, conditions); @@ -617,7 +617,7 @@ public async Task MatchOneAsync_GivenRulesetDateAndConditions_FetchesRulesForDay var validatorProvider = Mock.Of(); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await sut.MatchOneAsync(ruleset, matchDateTime, conditions); @@ -659,7 +659,7 @@ public async Task UpdateRuleAsync_GivenEmptyRuleDataSource_UpdatesRuleSuccesfull var validatorProvider = Mock.Of(); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); testRule.DateEnd = new DateTime(2019, 01, 02); testRule.Priority = 1; @@ -703,7 +703,7 @@ public async Task UpdateRuleAsync_GivenRuleWithInvalidDateEnd_UpdatesRuleFailure var validatorProvider = Mock.Of(); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); testRule.DateEnd = testRule.DateBegin.AddYears(-2); testRule.Priority = 1; @@ -752,7 +752,7 @@ public async Task VerifyParameters_GivenNullParameter_ThrowsArgumentNullExceptio .Returns(validator); var rulesEngineOptions = RulesEngineOptions.NewWithDefaults(); - var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, conditionTypeExtractorMock); + var sut = new RulesEngine(conditionsEvalEngineMock, rulesSourceMock, validatorProvider, rulesEngineOptions, ruleConditionsExtractorMock); // Act var actual = await Assert.ThrowsAsync(exceptionType, async () => @@ -839,7 +839,7 @@ public async Task VerifyParameters_GivenNullParameter_ThrowsArgumentNullExceptio private static Rule CreateTestStubRule() => Rule.Create("Test stub") - .InRuleset("Test content type") + .InRuleset("Test ruleset") .SetContent(new object()) .Since(DateTime.Parse("2024-08-17", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)) .Build() diff --git a/tests/Rules.Framework.Tests/Serialization/SerializedContentContainerTests.cs b/tests/Rules.Framework.Tests/Serialization/SerializedContentContainerTests.cs index 0e805d96..9fe5817e 100644 --- a/tests/Rules.Framework.Tests/Serialization/SerializedContentContainerTests.cs +++ b/tests/Rules.Framework.Tests/Serialization/SerializedContentContainerTests.cs @@ -13,7 +13,7 @@ public class SerializedContentContainerTests public void Init_GivenSerializedContent_DeserializesAndReturnsWhenFetchingContent() { // Arrange - var expectedContentType = RulesetNames.Type1.ToString(); + var expectedRuleset = RulesetNames.Type1.ToString(); var serializedContent = new object(); object expected = 19m; @@ -22,10 +22,10 @@ public void Init_GivenSerializedContent_DeserializesAndReturnsWhenFetchingConten .Returns(expected); var mockContentSerializationProvider = new Mock(); - mockContentSerializationProvider.Setup(x => x.GetContentSerializer(It.Is(y => y == expectedContentType))) + mockContentSerializationProvider.Setup(x => x.GetContentSerializer(It.Is(y => y == expectedRuleset))) .Returns(mockContentSerializer.Object); - var sut = new SerializedContentContainer(expectedContentType, serializedContent, mockContentSerializationProvider.Object); + var sut = new SerializedContentContainer(expectedRuleset, serializedContent, mockContentSerializationProvider.Object); // Act var actual = sut.GetContentAs(); diff --git a/tests/Rules.Framework.Tests/Source/RulesSourceTests.cs b/tests/Rules.Framework.Tests/Source/RulesSourceTests.cs index 23a5b75d..ecacc8b2 100644 --- a/tests/Rules.Framework.Tests/Source/RulesSourceTests.cs +++ b/tests/Rules.Framework.Tests/Source/RulesSourceTests.cs @@ -216,7 +216,7 @@ var rulesSourceMiddlewares var rulesDataSource = Mock.Of(); Mock.Get(rulesDataSource) - .Setup(x => x.GetRulesAsync(It.IsIn(getRulesArgs.ContentType), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd))) + .Setup(x => x.GetRulesAsync(It.IsIn(getRulesArgs.Ruleset), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd))) .ReturnsAsync(expected); RulesSource rulesSource = new(rulesDataSource, rulesSourceMiddlewares); @@ -228,7 +228,7 @@ var rulesSourceMiddlewares actual.Should().NotBeNullOrEmpty() .And.BeEquivalentTo(expected); Mock.Get(rulesDataSource) - .Verify(x => x.GetRulesAsync(It.IsIn(getRulesArgs.ContentType), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd)), Times.Once()); + .Verify(x => x.GetRulesAsync(It.IsIn(getRulesArgs.Ruleset), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd)), Times.Once()); } [Fact] @@ -246,7 +246,7 @@ public async Task GetRulesAsync_OneMiddleware_CallsMiddlewareAndRulesDataSourceA var rulesDataSource = Mock.Of(); Mock.Get(rulesDataSource) - .Setup(x => x.GetRulesAsync(It.IsIn(getRulesArgs.ContentType), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd))) + .Setup(x => x.GetRulesAsync(It.IsIn(getRulesArgs.Ruleset), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd))) .ReturnsAsync(expected); RulesSource rulesSource = new(rulesDataSource, rulesSourceMiddlewares); @@ -259,7 +259,7 @@ public async Task GetRulesAsync_OneMiddleware_CallsMiddlewareAndRulesDataSourceA .And.Contain(expected); middleware1.GetRulesCalls.Should().Be(1); Mock.Get(rulesDataSource) - .Verify(x => x.GetRulesAsync(It.IsIn(getRulesArgs.ContentType), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd)), Times.Once()); + .Verify(x => x.GetRulesAsync(It.IsIn(getRulesArgs.Ruleset), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd)), Times.Once()); } [Fact] @@ -279,7 +279,7 @@ public async Task GetRulesAsync_TwoMiddlewares_CallsFirstMiddlewareSecondMiddlew var rulesDataSource = Mock.Of(); Mock.Get(rulesDataSource) - .Setup(x => x.GetRulesAsync(It.IsIn(getRulesArgs.ContentType), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd))) + .Setup(x => x.GetRulesAsync(It.IsIn(getRulesArgs.Ruleset), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd))) .ReturnsAsync(expected); RulesSource rulesSource = new(rulesDataSource, rulesSourceMiddlewares); @@ -294,7 +294,7 @@ public async Task GetRulesAsync_TwoMiddlewares_CallsFirstMiddlewareSecondMiddlew middleware2.GetRulesCalls.Should().Be(1); middlewareMessages.Should().ContainInOrder("Enter middleware1.", "Enter middleware2.", "Exit middleware2.", "Exit middleware1."); Mock.Get(rulesDataSource) - .Verify(x => x.GetRulesAsync(It.IsIn(getRulesArgs.ContentType), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd)), Times.Once()); + .Verify(x => x.GetRulesAsync(It.IsIn(getRulesArgs.Ruleset), It.IsIn(getRulesArgs.DateBegin), It.IsIn(getRulesArgs.DateEnd)), Times.Once()); } [Fact] @@ -303,8 +303,8 @@ public async Task GetRulesetsAsync_NoMiddlewares_CallsRulesDataSource() // Arrange var expected = new[] { - new Ruleset("Content Type 1", DateTime.UtcNow), - new Ruleset("Content Type 2", DateTime.UtcNow), + new Ruleset("Ruleset 1", DateTime.UtcNow), + new Ruleset("Ruleset 2", DateTime.UtcNow), }; var getRulesetsArgs = new GetRulesetsArgs(); @@ -335,8 +335,8 @@ public async Task GetRulesetsAsync_OneMiddleware_CallsMiddlewareAndRulesDataSour // Arrange var expected = new[] { - new Ruleset("Content Type 1", DateTime.UtcNow), - new Ruleset("Content Type 2", DateTime.UtcNow), + new Ruleset("Ruleset 1", DateTime.UtcNow), + new Ruleset("Ruleset 2", DateTime.UtcNow), }; var getRulesetsArgs = new GetRulesetsArgs(); @@ -369,8 +369,8 @@ public async Task GetRulesetsAsync_TwoMiddlewares_CallsFirstMiddlewareSecondMidd // Arrange var expected = new[] { - new Ruleset("Content Type 1", DateTime.UtcNow), - new Ruleset("Content Type 2", DateTime.UtcNow), + new Ruleset("Ruleset 1", DateTime.UtcNow), + new Ruleset("Ruleset 2", DateTime.UtcNow), }; var getRulesetsArgs = new GetRulesetsArgs(); @@ -599,7 +599,7 @@ public async Task UpdateRuleAsync_TwoMiddlewares_CallsFirstMiddlewareSecondMiddl private static GetRulesArgs CreateGetRulesArgs() => new() { - ContentType = RulesetNames.Type1.ToString(), + Ruleset = RulesetNames.Type1.ToString(), DateBegin = DateTime.Parse("2022-01-01Z"), DateEnd = DateTime.Parse("2023-01-01Z"), }; diff --git a/tests/Rules.Framework.Tests/Source/StubRulesSourceMiddleware.cs b/tests/Rules.Framework.Tests/Source/StubRulesSourceMiddleware.cs index 9e58c1fd..27b5c238 100644 --- a/tests/Rules.Framework.Tests/Source/StubRulesSourceMiddleware.cs +++ b/tests/Rules.Framework.Tests/Source/StubRulesSourceMiddleware.cs @@ -57,9 +57,9 @@ public async Task> HandleGetRulesetsAsync(GetRulesetsArgs a { this.GetRulesetsCalls++; this.middlewareMessages.Add($"Enter {this.Name}."); - var contentTypes = await next.Invoke(args).ConfigureAwait(false); + var rulesets = await next.Invoke(args).ConfigureAwait(false); this.middlewareMessages.Add($"Exit {this.Name}."); - return contentTypes; + return rulesets; } public async Task> HandleGetRulesFilteredAsync( diff --git a/tests/Rules.Framework.Tests/Validation/SearchArgsValidatorTests.cs b/tests/Rules.Framework.Tests/Validation/SearchArgsValidatorTests.cs index 45c1d572..8d612a24 100644 --- a/tests/Rules.Framework.Tests/Validation/SearchArgsValidatorTests.cs +++ b/tests/Rules.Framework.Tests/Validation/SearchArgsValidatorTests.cs @@ -78,15 +78,32 @@ public void Validate_GivenConditionWithTypeAsEnumTypeAndDefinedValue_ReturnsSucc } [Fact] - public void Validate_GivenContentTypeAsClassTypeAndNotNullValue_ReturnsSuccessValidation() + public void Validate_GivenDateEndLesserThanDateEnd_ReturnsFailedValidation() + { + // Arrange + var searchArgs = new SearchArgs(RulesetNames.Type1, DateTime.Parse("2021-03-01Z"), DateTime.Parse("2021-02-01Z")); + + var validator = new SearchArgsValidator(); + + // Act + var validationResult = validator.Validate(searchArgs); + + // Assert + validationResult.IsValid.Should().BeFalse(); + validationResult.Errors.Should().HaveCount(1); + validationResult.Errors.Should().Match(c => c.Any(vf => vf.PropertyName == nameof(searchArgs.DateEnd))); + } + + [Fact] + public void Validate_GivenRulesetAsClassTypeAndNotNullValue_ReturnsSuccessValidation() { // Arrange - var contentType = new RulesetClass + var ruleset = new RulesetClass { Id = 1, Name = "Sample" }; - var searchArgs = new SearchArgs(contentType, DateTime.MinValue, DateTime.MaxValue); + var searchArgs = new SearchArgs(ruleset, DateTime.MinValue, DateTime.MaxValue); var validator = new SearchArgsValidator(); @@ -99,7 +116,7 @@ public void Validate_GivenContentTypeAsClassTypeAndNotNullValue_ReturnsSuccessVa } [Fact] - public void Validate_GivenContentTypeAsClassTypeAndNullValue_ReturnsFailedValidation() + public void Validate_GivenRulesetAsClassTypeAndNullValue_ReturnsFailedValidation() { // Arrange var searchArgs = new SearchArgs(null, DateTime.MinValue, DateTime.MaxValue); @@ -116,7 +133,7 @@ public void Validate_GivenContentTypeAsClassTypeAndNullValue_ReturnsFailedValida } [Fact] - public void Validate_GivenContentTypeAsEnumTypeAndDefinedValue_ReturnsSuccessValidation() + public void Validate_GivenRulesetAsEnumTypeAndDefinedValue_ReturnsSuccessValidation() { // Arrange var searchArgs = new SearchArgs(RulesetNames.Type1, DateTime.MinValue, DateTime.MaxValue); @@ -132,7 +149,7 @@ public void Validate_GivenContentTypeAsEnumTypeAndDefinedValue_ReturnsSuccessVal } [Fact] - public void Validate_GivenContentTypeAsEnumTypeAndUndefinedValue_ReturnsFailedValidation() + public void Validate_GivenRulesetAsEnumTypeAndUndefinedValue_ReturnsFailedValidation() { // Arrange var searchArgs = new SearchArgs(0, DateTime.MinValue, DateTime.MaxValue); @@ -147,22 +164,5 @@ public void Validate_GivenContentTypeAsEnumTypeAndUndefinedValue_ReturnsFailedVa validationResult.Errors.Should().HaveCount(1); validationResult.Errors.Should().Match(c => c.Any(vf => vf.PropertyName == nameof(searchArgs.Ruleset))); } - - [Fact] - public void Validate_GivenDateEndLesserThanDateEnd_ReturnsFailedValidation() - { - // Arrange - var searchArgs = new SearchArgs(RulesetNames.Type1, DateTime.Parse("2021-03-01Z"), DateTime.Parse("2021-02-01Z")); - - var validator = new SearchArgsValidator(); - - // Act - var validationResult = validator.Validate(searchArgs); - - // Assert - validationResult.IsValid.Should().BeFalse(); - validationResult.Errors.Should().HaveCount(1); - validationResult.Errors.Should().Match(c => c.Any(vf => vf.PropertyName == nameof(searchArgs.DateEnd))); - } } } \ No newline at end of file From 379771a8dd05daf2252f8ca488b4eaac41b37ef5 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sat, 2 Nov 2024 18:00:14 +0000 Subject: [PATCH 13/20] chore: add debugger display format for RQL runtime types --- src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs | 9 +++++---- src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs | 2 ++ .../Runtime/Types/RqlReadOnlyObject.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs | 2 ++ src/Rules.Framework.Rql/Runtime/Types/RqlString.cs | 3 ++- src/Rules.Framework.Rql/Runtime/Types/RqlType.cs | 4 +++- 13 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs index 76328de9..08c2f7c0 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs @@ -1,8 +1,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; + using System.Diagnostics; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}> (<{this.UnderlyingType.Name,nq}>)")] public readonly struct RqlAny : IRuntimeValue, IEquatable { private static readonly RqlType type = RqlTypes.Any; diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs index 6b444e11..d2c82f9b 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs @@ -1,10 +1,11 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; - using System.Collections.Generic; + using System.Diagnostics; using System.Text; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}>")] public readonly struct RqlArray : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(object[]); @@ -46,7 +47,7 @@ internal RqlArray(int size, bool shouldInitializeElements) public static object[] ConvertToNativeArray(RqlArray rqlArray) { var result = new object[rqlArray.size]; - for (int i = 0; i < rqlArray.size; i++) + for (var i = 0; i < rqlArray.size; i++) { result[i] = rqlArray.Value[i].RuntimeValue; } @@ -63,7 +64,7 @@ public bool Equals(RqlArray other) return false; } - for (int i = 0; i < this.size; i++) + for (var i = 0; i < this.size; i++) { if (!this.Value[i].Equals(other.Value[i])) { @@ -103,7 +104,7 @@ internal string ToString(int indent) .Append('{') .AppendLine(); var min = Math.Min(this.size, 5); - for (int i = 0; i < min; i++) + for (var i = 0; i < min; i++) { stringBuilder.Append(new string(' ', indent + 4)) .Append(this.Value[i]); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs index be1db973..c1bd052e 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs @@ -1,8 +1,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; + using System.Diagnostics; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}> {this.Value,nq}")] public readonly struct RqlBool : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(bool); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs index d1b5a177..9ab45ba3 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs @@ -1,8 +1,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; + using System.Diagnostics; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}> {this.Value.ToString(\"g\"),nq}")] public readonly struct RqlDate : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(DateTime); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs index 4a4f9ccf..ed3a340a 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs @@ -1,8 +1,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; + using System.Diagnostics; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}> {this.Value,nq}")] public readonly struct RqlDecimal : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(decimal); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs index 306f2f44..ad8252a1 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs @@ -1,8 +1,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; + using System.Diagnostics; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}> {this.Value,nq}")] public readonly struct RqlInteger : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(int); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs index 3909fc05..36c8ac68 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs @@ -1,8 +1,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; + using System.Diagnostics; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}>")] public readonly struct RqlNothing : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(object); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs index 2d872f85..057e3c04 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs @@ -2,9 +2,11 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Text; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}>")] public readonly struct RqlObject : IRuntimeValue, IPropertySet, IEquatable { private static readonly Type runtimeType = typeof(object); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs index 481ed1e2..fd7028b3 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs @@ -2,8 +2,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Text; + [DebuggerDisplay("<{this.Type.Name,nq}>")] public readonly struct RqlReadOnlyObject : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(object); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs index 5241daa2..9114d3fe 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs @@ -2,10 +2,12 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Text; using Rules.Framework.ConditionNodes; + [DebuggerDisplay("<{this.Type.Name,nq}> ({this.Value.Priority}) {this.Value.Name, nq}")] public readonly struct RqlRule : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(Rule); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs index 5d2aec8f..99b8e66d 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlRuleset.cs @@ -1,7 +1,9 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; + using System.Diagnostics; + [DebuggerDisplay("<{this.Type.Name,nq}> {this.Value.Name,nq}")] public readonly struct RqlRuleset : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(Ruleset); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs index 396c06c9..486f19ab 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs @@ -1,9 +1,10 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; - using System.Collections.Generic; + using System.Diagnostics; using Rules.Framework.Rql.Runtime; + [DebuggerDisplay("<{this.Type.Name,nq}> {this.Value}")] public readonly struct RqlString : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(string); diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs index e80791e7..490a0d40 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs @@ -2,7 +2,9 @@ namespace Rules.Framework.Rql.Runtime.Types { using System; using System.Collections.Generic; + using System.Diagnostics; + [DebuggerDisplay("RQL Type: {this.Name,nq}")] public readonly struct RqlType : IEquatable { private readonly IDictionary assignableTypes; @@ -40,7 +42,7 @@ public bool IsAssignableTo(RqlType rqlType) internal void AddAssignableType(RqlType rqlType) { - string rqlTypeName = rqlType.Name; + var rqlTypeName = rqlType.Name; if (string.Equals(rqlTypeName, this.Name, StringComparison.Ordinal)) { throw new InvalidOperationException("Type already is assignable to itself."); From a3666a637e4a1ae6a47a039063e8e0d06a19b5da Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sat, 2 Nov 2024 18:52:06 +0000 Subject: [PATCH 14/20] refactor: segregate rules grid to a separate component --- rules-framework.sln | 2 +- .../RuleConditionHierarchicalAccordion.razor | 85 +++++------- .../Components/PageComponents/RulesGrid.razor | 112 +++++++++++++++ .../Components/Pages/SearchRules.razor | 130 ++---------------- .../ComposedConditionNodeViewModel.cs | 6 +- .../ViewModels/ConditionNodeViewModel.cs | 6 +- .../ViewModels/RuleViewModel.cs | 6 +- .../ViewModels/ValueConditionNodeViewModel.cs | 6 +- 8 files changed, 174 insertions(+), 179 deletions(-) create mode 100644 src/Rules.Framework.WebUI/Components/PageComponents/RulesGrid.razor diff --git a/rules-framework.sln b/rules-framework.sln index 2369b569..0962b204 100644 --- a/rules-framework.sln +++ b/rules-framework.sln @@ -46,7 +46,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules.Framework.Rql", "src\ 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules.Framework.Rql.IntegrationTests", "tests\Rules.Framework.Rql.IntegrationTests\Rules.Framework.Rql.IntegrationTests.csproj", "{C24A2234-AD6A-4377-9FAA-9CC58386107C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor b/src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor index 917aa739..e614051d 100644 --- a/src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor +++ b/src/Rules.Framework.WebUI/Components/PageComponents/RuleConditionHierarchicalAccordion.razor @@ -17,7 +17,7 @@ @foreach (var conditionNode in this.ConditionNodes) { - if (conditionNode is ComposedConditionNode composedConditionNode) + if (conditionNode is ComposedConditionNodeViewModel composedConditionNode) { @@ -25,9 +25,9 @@ } - else if (conditionNode is ValueConditionNode valueConditionNode) + else if (conditionNode is ValueConditionNodeViewModel valueConditionNode) { - var shortValueConditionText = $"{valueConditionNode.Condition} {valueConditionNode.Operator} {valueConditionNode.GetOperandPrettyPrint()}"; + var shortValueConditionText = $"{valueConditionNode.Condition} {valueConditionNode.Operator} {GetOperandPrettyPrint(valueConditionNode.Operand)}";
    @@ -46,7 +46,7 @@

    Operator:

    @(valueConditionNode.Operator)

    -

    Operand:

    @(valueConditionNode.GetOperandPrettyPrint())

    +

    Operand:

    @(GetOperandPrettyPrint(valueConditionNode.Operand))

    @@ -73,7 +73,7 @@ } [Parameter] - public IEnumerable ConditionNodes { get; set; } = Enumerable.Empty(); + public IEnumerable ConditionNodes { get; set; } = Enumerable.Empty(); [Parameter] public bool EnableCollapseAllButton { get; set; } = false; @@ -105,66 +105,45 @@ } } - public sealed class ComposedConditionNode : ConditionNode + internal string GetOperandPrettyPrint(object operand) { - public IEnumerable ChildConditionNodes { get; internal set; } - } - - public class ConditionNode - { - public string LogicalOperator { get; internal set; } - } - - public sealed class ValueConditionNode : ConditionNode - { - public string Condition { get; internal set; } - - public string DataType { get; internal set; } - - public dynamic Operand { get; internal set; } - - public string Operator { get; internal set; } - - internal string GetOperandPrettyPrint() + var operandPrettyPrintBuilder = new StringBuilder(); + if (operand is IEnumerable elements && elements is not string) { - var operandPrettyPrintBuilder = new StringBuilder(); - if (this.Operand is IEnumerable elements && elements is not string) + operandPrettyPrintBuilder.Append("{ "); + var elementsProcessedCount = 0; + foreach (var element in elements) { - operandPrettyPrintBuilder.Append("{ "); - var elementsProcessedCount = 0; - foreach (var element in elements) + if (elementsProcessedCount >= 10) { - if (elementsProcessedCount >= 10) - { - operandPrettyPrintBuilder.Append(", ..."); - } - else if (elementsProcessedCount > 0) - { - operandPrettyPrintBuilder.Append(','); - } - - operandPrettyPrintBuilder.Append(element switch - { - null => "(null)", - "" => "(empty)", - _ => element, - }); - elementsProcessedCount++; + operandPrettyPrintBuilder.Append(", ..."); + } + else if (elementsProcessedCount > 0) + { + operandPrettyPrintBuilder.Append(','); } - operandPrettyPrintBuilder.Append(" }"); - } - else - { - operandPrettyPrintBuilder.Append(this.Operand switch + operandPrettyPrintBuilder.Append(element switch { null => "(null)", "" => "(empty)", - _ => this.Operand, + _ => element, }); + elementsProcessedCount++; } - return operandPrettyPrintBuilder.ToString(); + operandPrettyPrintBuilder.Append(" }"); } + else + { + operandPrettyPrintBuilder.Append(operand switch + { + null => "(null)", + "" => "(empty)", + _ => operand, + }); + } + + return operandPrettyPrintBuilder.ToString(); } } diff --git a/src/Rules.Framework.WebUI/Components/PageComponents/RulesGrid.razor b/src/Rules.Framework.WebUI/Components/PageComponents/RulesGrid.razor new file mode 100644 index 00000000..07bc13e3 --- /dev/null +++ b/src/Rules.Framework.WebUI/Components/PageComponents/RulesGrid.razor @@ -0,0 +1,112 @@ +@attribute [ExcludeFromCodeCoverage] +@rendermode InteractiveServer +@using System.Text.Json +@using System.Text.Json.Serialization + +
    + + + + @(context.Priority) + + + @(context.Ruleset) + + + @(context.Name) + + + @(context.DateBegin) + + + @(context.DateEnd) + + + @(JsonSerializer.Serialize(context.Content, this.jsonSerializerOptions)) + + + @if (context.Active) + { + Active + } + else + { + Deactivated + } + + + + + + +
    +
    +                                
    +                                    @(JsonSerializer.Serialize(context.RootCondition, this.jsonSerializerOptions))
    +                                
    +                            
    +
    +
    +
    + + +
    + @if (context.RootCondition is not null) + { + var conditionNodes = new[] { context.RootCondition }; + + } +
    +
    +
    +
    +
    +
    +
    + +@code { + private const int DefaultPageSize = 5; + private JsonSerializerOptions jsonSerializerOptions; + private Grid rulesGrid; + + public RulesGrid() + { + this.jsonSerializerOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + WriteIndented = true, + }; + this.jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + this.jsonSerializerOptions.Converters.Add(new PolymorphicWriteOnlyJsonConverter()); + } + + [Parameter] + public RulesGridProviderDelegate DataProvider { get; set; } + + public async Task RefreshAsync() + { + await this.rulesGrid.RefreshDataAsync(); + } + + private async Task> LoadRulesAsync(GridDataProviderRequest request) + { + var rules = await this.DataProvider.Invoke(); + return request.ApplyTo(rules); + } + + public delegate Task> RulesGridProviderDelegate(); +} diff --git a/src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor b/src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor index 4a8b568f..6701b516 100644 --- a/src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor +++ b/src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor @@ -101,79 +101,7 @@ -
    - - - - @(context.Priority) - - - @(context.Ruleset) - - - @(context.Name) - - - @(context.DateBegin) - - - @(context.DateEnd) - - - @(JsonSerializer.Serialize(context.Content, this.jsonSerializerOptions)) - - - @if (context.Active) - { - Active - } - else - { - Deactivated - } - - - - - - -
    -
    -                                            
    -                                                @(JsonSerializer.Serialize(context.RootCondition, this.jsonSerializerOptions))
    -                                            
    -                                        
    -
    -
    -
    - - -
    - @if (context.RootCondition is not null) - { - var conditionNodes = new[] { ConvertToComponentModel(context.RootCondition) }; - - } -
    -
    -
    -
    -
    -
    -
    + @@ -182,12 +110,7 @@ @code { private bool allRulesetsSelected; - private const int DefaultPageSize = 5; - private DateTime? dateBegin; - private DateInput dateBeginSearch; - private DateTime? dateEnd; private Guid instanceId; - private JsonSerializerOptions jsonSerializerOptions; private string exportFileName; private Modal exportModal; private string exportText; @@ -195,18 +118,10 @@ private string priorityCriteriaTooltip; private HashSet rulesetIds; private List rulesets; - private Grid rulesGrid; + private RulesGrid rulesGrid; public SearchRules() { - this.jsonSerializerOptions = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IncludeFields = true, - WriteIndented = true, - }; - this.jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - this.jsonSerializerOptions.Converters.Add(new PolymorphicWriteOnlyJsonConverter()); this.rulesetIds = new HashSet(); } @@ -260,32 +175,11 @@ } this.StateHasChanged(); - await this.rulesGrid.RefreshDataAsync(); + await this.rulesGrid.RefreshAsync(); } } - private RuleConditionHierarchicalAccordion.ConditionNode ConvertToComponentModel(ConditionNodeViewModel viewModel) - { - return viewModel switch - { - ComposedConditionNodeViewModel composedConditionNodeViewModel => new RuleConditionHierarchicalAccordion.ComposedConditionNode - { - ChildConditionNodes = composedConditionNodeViewModel.ChildConditionNodes.Select(x => ConvertToComponentModel(x)), - LogicalOperator = composedConditionNodeViewModel.LogicalOperator, - }, - ValueConditionNodeViewModel valueConditionNodeViewModel => new RuleConditionHierarchicalAccordion.ValueConditionNode - { - Condition = valueConditionNodeViewModel.Condition, - DataType = valueConditionNodeViewModel.DataType, - LogicalOperator = valueConditionNodeViewModel.LogicalOperator, - Operand = valueConditionNodeViewModel.Operand, - Operator = valueConditionNodeViewModel.Operator, - }, - _ => throw new NotSupportedException(), - }; - } - - private async Task> LoadRulesAsync(GridDataProviderRequest request) + private async Task> LoadRulesAsync() { List ruleViewModels = new List(); if (this.instanceId != Guid.Empty && this.rulesetIds.Any()) @@ -297,17 +191,15 @@ ? rulesets : rulesets.Where(r => this.rulesetIds.Contains(GuidGenerator.GenerateFromString(r.Name))); - var searchDateBegin = this.dateBegin.GetValueOrDefault(DateTime.MinValue); - var searchDateEnd = this.dateEnd.GetValueOrDefault(DateTime.MaxValue); foreach (var ruleset in rulesetsToSearch) { - var rules = await instance.RulesEngine.SearchAsync(new SearchArgs(ruleset.Name, searchDateBegin, searchDateEnd)); + var rules = await instance.RulesEngine.SearchAsync(new SearchArgs(ruleset.Name, DateTime.MinValue, DateTime.MaxValue)); ruleViewModels.AddRange(rules.OrderBy(r => r.Priority).Select(r => r.ToViewModel())); } } - return request.ApplyTo(ruleViewModels); + return ruleViewModels; } private async Task OnRulesetCheckAsync(Guid rulesetId) @@ -343,19 +235,15 @@ await this.Storage.SetAsync(WebUIConstants.SelectedRulesetsStorageKey, this.rulesetIds); this.StateHasChanged(); - await this.rulesGrid.RefreshDataAsync(); + await this.rulesGrid.RefreshAsync(); } private async Task OnExportJsonButtonClickAsync() { if (this.rulesetIds.Any()) { - var request = new GridDataProviderRequest - { - Filters = this.rulesGrid.GetFilters(), - }; - var response = await this.rulesGrid.DataProvider.Invoke(request); - var rules = response.Data.Select(r => r.ToExportRulesModel()).ToArray(); + var response = await this.rulesGrid.DataProvider.Invoke(); + var rules = response.Select(r => r.ToExportRulesModel()).ToArray(); if (this.rulesetIds.Count > 1) { this.exportFileName = "selection-rules"; diff --git a/src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs index ef46e62e..55aeadfd 100644 --- a/src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs +++ b/src/Rules.Framework.WebUI/ViewModels/ComposedConditionNodeViewModel.cs @@ -2,8 +2,12 @@ namespace Rules.Framework.WebUI.ViewModels { using System.Collections.Generic; - internal sealed class ComposedConditionNodeViewModel : ConditionNodeViewModel + public sealed class ComposedConditionNodeViewModel : ConditionNodeViewModel { + internal ComposedConditionNodeViewModel() + { + } + public IEnumerable ChildConditionNodes { get; internal set; } } } \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs index e1c1ec46..548dda87 100644 --- a/src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs +++ b/src/Rules.Framework.WebUI/ViewModels/ConditionNodeViewModel.cs @@ -1,7 +1,11 @@ namespace Rules.Framework.WebUI.ViewModels { - internal class ConditionNodeViewModel + public class ConditionNodeViewModel { + internal ConditionNodeViewModel() + { + } + public string LogicalOperator { get; internal set; } } } \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs index ac3b6c59..3dfbf3ec 100644 --- a/src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs +++ b/src/Rules.Framework.WebUI/ViewModels/RuleViewModel.cs @@ -2,8 +2,12 @@ namespace Rules.Framework.WebUI.ViewModels { using System; - internal sealed class RuleViewModel + public sealed class RuleViewModel { + internal RuleViewModel() + { + } + public bool Active { get; set; } public string Conditions { get; set; } diff --git a/src/Rules.Framework.WebUI/ViewModels/ValueConditionNodeViewModel.cs b/src/Rules.Framework.WebUI/ViewModels/ValueConditionNodeViewModel.cs index db128258..b48ae3dc 100644 --- a/src/Rules.Framework.WebUI/ViewModels/ValueConditionNodeViewModel.cs +++ b/src/Rules.Framework.WebUI/ViewModels/ValueConditionNodeViewModel.cs @@ -1,7 +1,11 @@ namespace Rules.Framework.WebUI.ViewModels { - internal sealed class ValueConditionNodeViewModel : ConditionNodeViewModel + public sealed class ValueConditionNodeViewModel : ConditionNodeViewModel { + internal ValueConditionNodeViewModel() + { + } + public string Condition { get; internal set; } public string DataType { get; internal set; } From da6a4ffa8d012045e1ec22c3537f7b8b7da35b3c Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Fri, 15 Nov 2024 10:42:54 +0000 Subject: [PATCH 15/20] feat: add RQL terminal to Web UI --- .../Rules.Framework.WebUI.Sample/Program.cs | 12 + .../Rules.Framework.WebUI.Sample.csproj | 1 + src/Rules.Framework.WebUI/Assets/app.css | 3 + .../Components/Layout/NavMenu.razor | 6 + .../Components/Pages/RqlTerminal.razor | 262 ++++++++++++++++++ .../Components/Pages/RqlTerminal.razor.css | 27 ++ .../Components/Pages/SearchRules.razor | 2 - .../Components/WebUIApp.razor | 15 + .../RqlTerminalOptions.cs | 16 ++ src/Rules.Framework.WebUI/WebUIOptions.cs | 6 + 10 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 src/Rules.Framework.WebUI/Components/Pages/RqlTerminal.razor create mode 100644 src/Rules.Framework.WebUI/Components/Pages/RqlTerminal.razor.css create mode 100644 src/Rules.Framework.WebUI/RqlTerminalOptions.cs diff --git a/samples/Rules.Framework.WebUI.Sample/Program.cs b/samples/Rules.Framework.WebUI.Sample/Program.cs index 94ba3684..bd14b843 100644 --- a/samples/Rules.Framework.WebUI.Sample/Program.cs +++ b/samples/Rules.Framework.WebUI.Sample/Program.cs @@ -1,5 +1,7 @@ namespace Rules.Framework.WebUI.Sample { + using global::Rules.Framework.IntegrationTests.Common.Scenarios; + using global::Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8; using global::Rules.Framework.WebUI.Sample.Engine; using global::Rules.Framework.WebUI.Sample.ReadmeExample; using global::Rules.Framework.WebUI.Sample.Rules; @@ -23,6 +25,16 @@ public static void Main(string[] args) })); return await rulesProvider.GetRulesEngineAsync(); + }) + .AddInstance("Poker combinations example", async (_, _) => + { + var rulesEngine = RulesEngineBuilder.CreateRulesEngine() + .SetInMemoryDataSource() + .Build(); + + await ScenarioLoader.LoadScenarioAsync(rulesEngine, new Scenario8Data()); + + return rulesEngine; }); }); diff --git a/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj b/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj index c6467180..ac454b2f 100644 --- a/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj +++ b/samples/Rules.Framework.WebUI.Sample/Rules.Framework.WebUI.Sample.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Rules.Framework.WebUI/Assets/app.css b/src/Rules.Framework.WebUI/Assets/app.css index 7266677e..e95364b4 100644 --- a/src/Rules.Framework.WebUI/Assets/app.css +++ b/src/Rules.Framework.WebUI/Assets/app.css @@ -54,11 +54,14 @@ h1:focus { position: relative; display: flex; flex-direction: column; + width: 100%; } main { max-height: calc(100vh - 3.5rem); height: calc(100vh - 3.5rem); + padding-right: 0 !important; + margin-right: 0 !important; } .sidebar { diff --git a/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor b/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor index 3aeb3428..e09fafd7 100644 --- a/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor +++ b/src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor @@ -25,6 +25,12 @@ + + @if (this.shouldRenderSwitchInstanceLink) {