From c9aed1d8cc467981f9b4ad698b3f1b343f92a0bd Mon Sep 17 00:00:00 2001 From: Aida Crone Date: Sun, 15 Mar 2020 14:50:41 -0700 Subject: [PATCH] Initial commit --- .editorconfig | 4 + .gitignore | 353 ++++++++++++++++++ Liminality.sln | 93 +++++ .../AspNetCoreExample.csproj | 13 + .../Controllers/Covid19Controller.cs | 43 +++ examples/AspNetCoreExample/Program.cs | 26 ++ .../Properties/launchSettings.json | 30 ++ examples/AspNetCoreExample/SARSCoV2Assay.cs | 136 +++++++ examples/AspNetCoreExample/Startup.cs | 55 +++ .../appsettings.Development.json | 9 + examples/AspNetCoreExample/appsettings.json | 10 + ...lity.Extensions.DependencyInjection.csproj | 11 + .../ServiceCollectionExtensions.cs | 16 + src/Liminality/ISignalHandler.cs | 15 + src/Liminality/Liminality.csproj | 14 + src/Liminality/Resolver.cs | 86 +++++ src/Liminality/StateMachine.cs | 59 +++ src/Liminality/StateMachineBuilder.cs | 118 ++++++ test/Liminality.Tests/Liminality.Tests.csproj | 28 ++ test/Liminality.Tests/UnitTest1.cs | 23 ++ 20 files changed, 1142 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Liminality.sln create mode 100644 examples/AspNetCoreExample/AspNetCoreExample.csproj create mode 100644 examples/AspNetCoreExample/Controllers/Covid19Controller.cs create mode 100644 examples/AspNetCoreExample/Program.cs create mode 100644 examples/AspNetCoreExample/Properties/launchSettings.json create mode 100644 examples/AspNetCoreExample/SARSCoV2Assay.cs create mode 100644 examples/AspNetCoreExample/Startup.cs create mode 100644 examples/AspNetCoreExample/appsettings.Development.json create mode 100644 examples/AspNetCoreExample/appsettings.json create mode 100644 src/Liminality.Extensions.DependencyInjection/Liminality.Extensions.DependencyInjection.csproj create mode 100644 src/Liminality.Extensions.DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Liminality/ISignalHandler.cs create mode 100644 src/Liminality/Liminality.csproj create mode 100644 src/Liminality/Resolver.cs create mode 100644 src/Liminality/StateMachine.cs create mode 100644 src/Liminality/StateMachineBuilder.cs create mode 100644 test/Liminality.Tests/Liminality.Tests.csproj create mode 100644 test/Liminality.Tests/UnitTest1.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a5cd273 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS8625: Cannot convert null literal to non-nullable reference type. +dotnet_diagnostic.CS8625.severity = error diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..611428f --- /dev/null +++ b/.gitignore @@ -0,0 +1,353 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/Liminality.sln b/Liminality.sln new file mode 100644 index 0000000..29acbb3 --- /dev/null +++ b/Liminality.sln @@ -0,0 +1,93 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29806.167 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4A0234DD-6FB2-4874-AB2E-51E6B6812E6B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liminality", "src\Liminality\Liminality.csproj", "{1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8DD9418F-77D3-49F0-B9AF-1246A172C150}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liminality.Tests", "test\Liminality.Tests\Liminality.Tests.csproj", "{F5764FD1-C398-44CC-A0EF-29B90C8240B2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C59FF830-CF29-4409-BFD4-E79CAB6D24FC}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liminality.Extensions.DependencyInjection", "src\Liminality.Extensions.DependencyInjection\Liminality.Extensions.DependencyInjection.csproj", "{BDF2E735-34CB-4060-84FE-D47837B01215}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreExample", "examples\AspNetCoreExample\AspNetCoreExample.csproj", "{880F84CE-C436-49FB-BC1A-55E68DD5CBCF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Debug|x64.Build.0 = Debug|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Debug|x86.Build.0 = Debug|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Release|Any CPU.Build.0 = Release|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Release|x64.ActiveCfg = Release|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Release|x64.Build.0 = Release|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Release|x86.ActiveCfg = Release|Any CPU + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31}.Release|x86.Build.0 = Release|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Debug|x64.Build.0 = Debug|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Debug|x86.Build.0 = Debug|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Release|Any CPU.Build.0 = Release|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Release|x64.ActiveCfg = Release|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Release|x64.Build.0 = Release|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Release|x86.ActiveCfg = Release|Any CPU + {F5764FD1-C398-44CC-A0EF-29B90C8240B2}.Release|x86.Build.0 = Release|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Debug|x64.ActiveCfg = Debug|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Debug|x64.Build.0 = Debug|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Debug|x86.ActiveCfg = Debug|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Debug|x86.Build.0 = Debug|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Release|Any CPU.Build.0 = Release|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Release|x64.ActiveCfg = Release|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Release|x64.Build.0 = Release|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Release|x86.ActiveCfg = Release|Any CPU + {BDF2E735-34CB-4060-84FE-D47837B01215}.Release|x86.Build.0 = Release|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Debug|x64.ActiveCfg = Debug|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Debug|x64.Build.0 = Debug|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Debug|x86.ActiveCfg = Debug|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Debug|x86.Build.0 = Debug|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Release|Any CPU.Build.0 = Release|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Release|x64.ActiveCfg = Release|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Release|x64.Build.0 = Release|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Release|x86.ActiveCfg = Release|Any CPU + {880F84CE-C436-49FB-BC1A-55E68DD5CBCF}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1A48C85D-3AB9-4ED7-8593-0DCF8681CF31} = {4A0234DD-6FB2-4874-AB2E-51E6B6812E6B} + {F5764FD1-C398-44CC-A0EF-29B90C8240B2} = {8DD9418F-77D3-49F0-B9AF-1246A172C150} + {BDF2E735-34CB-4060-84FE-D47837B01215} = {4A0234DD-6FB2-4874-AB2E-51E6B6812E6B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {59F436D9-777E-4A01-9738-D45A10A6438F} + EndGlobalSection +EndGlobal diff --git a/examples/AspNetCoreExample/AspNetCoreExample.csproj b/examples/AspNetCoreExample/AspNetCoreExample.csproj new file mode 100644 index 0000000..779cc7b --- /dev/null +++ b/examples/AspNetCoreExample/AspNetCoreExample.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + + + + + + + + diff --git a/examples/AspNetCoreExample/Controllers/Covid19Controller.cs b/examples/AspNetCoreExample/Controllers/Covid19Controller.cs new file mode 100644 index 0000000..031585c --- /dev/null +++ b/examples/AspNetCoreExample/Controllers/Covid19Controller.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace AspNetCoreExample.Controllers +{ + [ApiController] + [Route("assays/[controller]")] + public class Covid19Controller : ControllerBase + { + private readonly ILogger _logger; + + public Covid19Controller( + SARSCoV2Assay covid19Assay, + ILogger logger) + { + Covid19Assay = covid19Assay; + _logger = logger; + } + + public SARSCoV2Assay Covid19Assay { get; } + + [HttpPost] + public async ValueTask PostAsync(SARSCoV2Assay.BiologicalSequenceSample sample, CancellationToken cancellationToken = default) + { + var (success, result) = await Covid19Assay.SignalAsync(sample, cancellationToken).ConfigureAwait(false); + + if(!success) throw new Exception(); + + return result switch + { + SARSCoV2Assay.Positive _ => true, + SARSCoV2Assay.Negative _ => false, + SARSCoV2Assay.Inconclusive _ => null, + _ => throw new Exception() + }; + } + } +} diff --git a/examples/AspNetCoreExample/Program.cs b/examples/AspNetCoreExample/Program.cs new file mode 100644 index 0000000..91de459 --- /dev/null +++ b/examples/AspNetCoreExample/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace AspNetCoreExample +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/examples/AspNetCoreExample/Properties/launchSettings.json b/examples/AspNetCoreExample/Properties/launchSettings.json new file mode 100644 index 0000000..99d6954 --- /dev/null +++ b/examples/AspNetCoreExample/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59515", + "sslPort": 44338 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "AspNetCoreExample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/AspNetCoreExample/SARSCoV2Assay.cs b/examples/AspNetCoreExample/SARSCoV2Assay.cs new file mode 100644 index 0000000..3abe72a --- /dev/null +++ b/examples/AspNetCoreExample/SARSCoV2Assay.cs @@ -0,0 +1,136 @@ +using PSIBR.Liminality; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AspNetCoreExample +{ + using TransitionMap = Dictionary<(Type CurrentState, Type Signal), (Type Precondition, Type NewState)>; + + public class SARSCoV2Assay : StateMachine + { + public SARSCoV2Assay( + /* Here you could add a repository or eventstream as a dependency */ + Resolver resolver) + : base(resolver) + { + } + + protected override TransitionMap Define(StateMachineBuilder stateMachineBuilder) => + stateMachineBuilder + .StartsIn() + .For().On().MoveTo() + .For().On().MoveTo() + .For().On().MoveTo() + .For().On().MoveTo() + .For().On().MoveTo() + .Build(); + + protected override ValueTask LoadStateAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + protected override ValueTask PersistStateAsync(object state, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + /// + /// Empty state + /// + public class Ready { } + + /// + /// A theoretical biological sequence sample. + /// https://www.ncbi.nlm.nih.gov/IEB/ToolBox/SDKDOCS/DATAMODL.HTML + /// + /// This isn't fully implemented, just enough for an example + public class BiologicalSequenceSample + { + [Required] + public string Id { get; set; } + + public string Descr { get; set; } + + public Sequence Inst { get; set; } + + public class Sequence + { + public string Data { get; set; } + } + + } + + public class Analyzing + : ISignalHandler + { + /// + /// Test for SARS-CoV-2 + /// + /// + /// + public ValueTask InvokeAsync(BiologicalSequenceSample sample, CancellationToken cancellationToken = default) + { + var result = new Analysis + { + Orf1Gene = sample.Inst.Data.Contains("ORF1"), + NGene = sample.Inst.Data.Contains("N"), + EGene = sample.Inst.Data.Contains("E") + }; + + // Signal with analysis + + return new ValueTask(); + } + } + + public class Analysis + { + public bool Orf1Gene { get; set; } + + public bool NGene { get; set; } + + public bool EGene { get; set; } + } + + public class Evaluating + : ISignalHandler + { + public ValueTask InvokeAsync(Analysis analysis, CancellationToken cancellationToken = default) + { + if (analysis.Orf1Gene && analysis.NGene && analysis.EGene) + { + // signal PositiveEvaluation + return new ValueTask(); + } + else if (!analysis.Orf1Gene && !analysis.NGene && !analysis.EGene) + { + // signal NegativeEvaluation + return new ValueTask(); + } + else + { + // signal InconclusiveEvaluation + return new ValueTask(); + } + + } + } + + public class PositiveEvaluation { } + + public class NegativeEvaluation { } + + public class InconclusiveEvaluation { } + + public class Positive { } + + public class Negative { } + + public class Inconclusive { } + } +} diff --git a/examples/AspNetCoreExample/Startup.cs b/examples/AspNetCoreExample/Startup.cs new file mode 100644 index 0000000..372bbfb --- /dev/null +++ b/examples/AspNetCoreExample/Startup.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Liminality.Extensions.DependencyInjection; + +namespace AspNetCoreExample +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddLiminality(); + services.AddTypedStateMachine(); + + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/examples/AspNetCoreExample/appsettings.Development.json b/examples/AspNetCoreExample/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/examples/AspNetCoreExample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/examples/AspNetCoreExample/appsettings.json b/examples/AspNetCoreExample/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/examples/AspNetCoreExample/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Liminality.Extensions.DependencyInjection/Liminality.Extensions.DependencyInjection.csproj b/src/Liminality.Extensions.DependencyInjection/Liminality.Extensions.DependencyInjection.csproj new file mode 100644 index 0000000..29d56dc --- /dev/null +++ b/src/Liminality.Extensions.DependencyInjection/Liminality.Extensions.DependencyInjection.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Liminality.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Liminality.Extensions.DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..79e7a36 --- /dev/null +++ b/src/Liminality.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Liminality.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static void AddLiminality(this IServiceCollection services) + { + } + + public static void AddTypedStateMachine(this IServiceCollection services) + { + } + } +} diff --git a/src/Liminality/ISignalHandler.cs b/src/Liminality/ISignalHandler.cs new file mode 100644 index 0000000..44e755a --- /dev/null +++ b/src/Liminality/ISignalHandler.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PSIBR.Liminality +{ + public interface ISignalHandler + { + ValueTask InvokeAsync(TSignal signal, CancellationToken cancellationToken = default); + } + + public interface IPrecondition + { + ValueTask CheckAsync(TSignal signal, CancellationToken cancellationToken = default); + } +} diff --git a/src/Liminality/Liminality.csproj b/src/Liminality/Liminality.csproj new file mode 100644 index 0000000..4ad5e2d --- /dev/null +++ b/src/Liminality/Liminality.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.1 + PSIBR.Liminality + @aidapsibr, @zoeysaurusrawr + PSIBR, LLC + Apache-2.0 + Copyright © 2020 PSIBR, LLC + PSIBR.Liminality + enable + + + diff --git a/src/Liminality/Resolver.cs b/src/Liminality/Resolver.cs new file mode 100644 index 0000000..f7901d3 --- /dev/null +++ b/src/Liminality/Resolver.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PSIBR.Liminality +{ + using TransitionMap = Dictionary<(Type CurrentState, Type Signal), (Type? Precondition, Type NewState)>; + + public class Resolver + { + private readonly IServiceProvider _serviceProvider; + + public Resolver(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public MaybeResolution Resolve(TransitionMap transitionMap, Type state) + where TSignal : class, new() + => transitionMap.TryGetValue((state, typeof(TSignal)), out var destination) + ? new MaybeResolution(new Resolution(/* TODO: Actually resolve */)) + : new MaybeResolution(); + } + + public struct MaybeResolution where TSignal : class, new() + { + public bool foundMatch; + public Resolution resolution; + + public MaybeResolution(Resolution resolution) + { + this.foundMatch = true; + this.resolution = resolution; + } + + public override bool Equals(object? obj) + { + return obj is MaybeResolution other && + foundMatch == other.foundMatch && + EqualityComparer>.Default.Equals(resolution, other.resolution); + } + + public override int GetHashCode() + { + return HashCode.Combine(foundMatch, resolution); + } + + public void Deconstruct(out bool foundMatch, out Resolution resolution) + { + foundMatch = this.foundMatch; + resolution = this.resolution; + } + } + + public struct Resolution + where TSignal : class, new() + { + public IPrecondition Precondition; + public ISignalHandler State; + + public Resolution(IPrecondition precondition, ISignalHandler state) + { + this.Precondition = precondition; + this.State = state; + } + + public override bool Equals(object? obj) + { + return obj is Resolution other && + EqualityComparer>.Default.Equals(Precondition, other.Precondition) && + EqualityComparer>.Default.Equals(State, other.State); + } + + public override int GetHashCode() + { + return HashCode.Combine(Precondition, State); + } + + public void Deconstruct(out IPrecondition precondition, out ISignalHandler state) + { + precondition = this.Precondition; + state = this.State; + } + } +} diff --git a/src/Liminality/StateMachine.cs b/src/Liminality/StateMachine.cs new file mode 100644 index 0000000..09241f1 --- /dev/null +++ b/src/Liminality/StateMachine.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PSIBR.Liminality +{ + using TransitionMap = Dictionary<(Type CurrentState, Type Signal), (Type? Precondition, Type NewState)>; + + public abstract class StateMachine + { + private readonly Resolver _resolver; + private readonly Lazy _transitionMap; + + protected StateMachine(Resolver resolver) + { + _resolver = resolver; + _transitionMap = new Lazy(valueFactory: DefineApplied); + + // Use partial application to provide StateMachineBuilder to the Define method lazily + // https://en.wikipedia.org/wiki/Partial_application + TransitionMap DefineApplied() + { + return Define(new StateMachineBuilder()); + } + } + + public async ValueTask<(bool DidTransition, object State)> SignalAsync( + TSignal signal, + CancellationToken cancellationToken = default) + where TSignal : class, new() + { + var currentState = await LoadStateAsync(cancellationToken).ConfigureAwait(false); + + var (foundMatch, resolution) = _resolver.Resolve(_transitionMap.Value, currentState.GetType()); + + if (!foundMatch) return (false, currentState); + + var preconditionValueTask = resolution.Precondition.CheckAsync(signal, cancellationToken); + + if (!await preconditionValueTask.ConfigureAwait(false)) return (false, currentState); + + if (!await PersistStateAsync(resolution.State, cancellationToken).ConfigureAwait(false)) + return (false, currentState); + + var handlerValueTask = resolution.State.InvokeAsync(signal, cancellationToken); + + await handlerValueTask.ConfigureAwait(false); + + return (true, resolution.State); + } + + protected abstract TransitionMap Define(StateMachineBuilder stateMachineBuilder); + + protected abstract ValueTask LoadStateAsync(CancellationToken cancellationToken = default); + + protected abstract ValueTask PersistStateAsync(object state, CancellationToken cancellationToken = default); + } +} diff --git a/src/Liminality/StateMachineBuilder.cs b/src/Liminality/StateMachineBuilder.cs new file mode 100644 index 0000000..c836542 --- /dev/null +++ b/src/Liminality/StateMachineBuilder.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; + +namespace PSIBR.Liminality +{ + using TransitionMap = Dictionary<(Type CurrentState, Type Signal), (Type? Precondition, Type NewState)>; + + public class StateMachineBuilder + { + private Type? _initialState; + protected readonly TransitionMap TransitionMap = new TransitionMap(); + protected readonly StateBuilder _stateBuilder; + + public StateMachineBuilder() + { + _stateBuilder = new StateBuilder(this); + } + + public StateBuilder StartsIn() + where TState : class + { + _initialState = typeof(TState); + + return _stateBuilder; + } + + private StateBuilder AddTransition() + where TState : class + where TSignal : class, new() + where TNewState : class + { + TransitionMap[(typeof(TState), typeof(TSignal))] = (null, typeof(TNewState)); + + return _stateBuilder; + } + + private StateBuilder AddTransition() + where TState : class + where TSignal : class, new() + where TPrecondition : class + where TNewState : class + { + TransitionMap[(typeof(TState), typeof(TSignal))] = (typeof(TPrecondition), typeof(TNewState)); + + return _stateBuilder; + } + + public class StateBuilder + { + private readonly StateMachineBuilder _stateMachineBuilder; + + public StateBuilder(StateMachineBuilder stateMachineBuilder) + { + _stateMachineBuilder = stateMachineBuilder; + } + + public ForStateContext For() + where TState : class + => new ForStateContext(_stateMachineBuilder); + + public TransitionMap Build() => _stateMachineBuilder.TransitionMap; + } + + public class ForStateContext + where TState : class + { + private readonly StateMachineBuilder _stateMachineBuilder; + + public ForStateContext(StateMachineBuilder stateMachineBuilder) + { + _stateMachineBuilder = stateMachineBuilder; + } + + public OnContext On() + where TSignal : class, new() + => new OnContext(_stateMachineBuilder); + } + + public class OnContext + where TState : class + where TSignal : class, new() + { + private readonly StateMachineBuilder _stateMachineBuilder; + + public OnContext(StateMachineBuilder stateMachineBuilder) + { + _stateMachineBuilder = stateMachineBuilder; + } + + public StateBuilder MoveTo() + where TNewState : class + => _stateMachineBuilder.AddTransition(); + + public WhenContext When() + where TPrecondition : class, IPrecondition + => new WhenContext(_stateMachineBuilder); + } + + public class WhenContext + where TState : class + where TSignal : class, new() + where TPrecondition : class, IPrecondition + { + private readonly StateMachineBuilder _stateMachineBuilder; + + public WhenContext(StateMachineBuilder stateMachineBuilder) + { + _stateMachineBuilder = stateMachineBuilder; + } + + public StateBuilder MoveTo() + where TNewState : class + => _stateMachineBuilder.AddTransition(); + } + } + + +} \ No newline at end of file diff --git a/test/Liminality.Tests/Liminality.Tests.csproj b/test/Liminality.Tests/Liminality.Tests.csproj new file mode 100644 index 0000000..037439e --- /dev/null +++ b/test/Liminality.Tests/Liminality.Tests.csproj @@ -0,0 +1,28 @@ + + + + netcoreapp3.1 + PSIBR.Liminality + enable + + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test/Liminality.Tests/UnitTest1.cs b/test/Liminality.Tests/UnitTest1.cs new file mode 100644 index 0000000..3434b03 --- /dev/null +++ b/test/Liminality.Tests/UnitTest1.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace PSIBR.Liminality.Tests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + } + } + + // States + public class Idle { } + public class InProgress { } + public class Finished { } + + // Inputs + public class Start { } + public class Finish { } +}