From 84d6c1c7c568ff84cf15b83f41ad9c598c6f729c Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Thu, 12 Sep 2024 15:27:22 -0700 Subject: [PATCH] update (#17) --- StepWise.sln | 16 +- example/CodeInterpreter/Program.cs | 5 +- example/GetWeather/Program.cs | 5 +- nuget/NUGET.md | 6 +- nuget/icon.png | Bin 1490 -> 5955 bytes schema/StepWiseControllerV1.schema.json | 332 ++++++++++++++++++ .../Extension/StepWiseEngineExtension.cs | 4 +- src/StepWise.Core/IStepWiseEngine.cs | 7 +- src/StepWise.Core/Step.cs | 22 +- src/StepWise.Core/StepWise.Core.csproj | 4 + src/StepWise.Core/StepWiseEngine.cs | 16 +- src/StepWise.Core/Workflow.cs | 10 +- src/StepWise.WebAPI/DTO.cs | 55 +++ .../Extension/HostBuilderExtension.cs | 60 ++++ src/StepWise.WebAPI/StepWise.WebAPI.csproj | 24 ++ src/StepWise.WebAPI/StepWiseControllerV1.cs | 96 +++++ .../GuessNumberWorkflowTest.cs | 8 +- .../PrepareDinnerWorkflowTest.cs | 10 +- .../StepAndWorkflowTests.cs | 15 +- ...ControllerV1Tests.TestSwagger.approved.txt | 9 + ...ControllerV1Tests.TestSwagger.approved.txt | 332 ++++++++++++++++++ .../StepWise.WebAPI.Tests.csproj | 16 + ...ControllerV1Tests.TestSwagger.received.txt | 1 + ...ControllerV1Tests.TestSwagger.verified.txt | 9 + .../StepWiseControllerV1Tests.cs | 52 +++ 25 files changed, 1061 insertions(+), 53 deletions(-) create mode 100644 schema/StepWiseControllerV1.schema.json create mode 100644 src/StepWise.WebAPI/DTO.cs create mode 100644 src/StepWise.WebAPI/Extension/HostBuilderExtension.cs create mode 100644 src/StepWise.WebAPI/StepWise.WebAPI.csproj create mode 100644 src/StepWise.WebAPI/StepWiseControllerV1.cs create mode 100644 test/StepWise.WebAPI.Tests/ApprovalTests/StepWiseControllerV1Tests.TestSwagger.approved.txt create mode 100644 test/StepWise.WebAPI.Tests/Approvals/StepWiseControllerV1Tests.TestSwagger.approved.txt create mode 100644 test/StepWise.WebAPI.Tests/StepWise.WebAPI.Tests.csproj create mode 100644 test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.received.txt create mode 100644 test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.verified.txt create mode 100644 test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.cs diff --git a/StepWise.sln b/StepWise.sln index d7cc8d3..4fed9d9 100644 --- a/StepWise.sln +++ b/StepWise.sln @@ -23,7 +23,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetWeather", "example\GetWe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepWise.Core.Tests", "test\StepWise.Core.Tests\StepWise.Core.Tests.csproj", "{2C1A5352-C488-4EBB-B3C6-2FDCE9B043F8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeInterpreter", "example\CodeInterpreter\CodeInterpreter.csproj", "{9BF63586-31E6-4075-B120-C8D2B6E7296A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeInterpreter", "example\CodeInterpreter\CodeInterpreter.csproj", "{9BF63586-31E6-4075-B120-C8D2B6E7296A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StepWise.WebAPI", "src\StepWise.WebAPI\StepWise.WebAPI.csproj", "{8D2B6D19-3922-4B52-BB88-B28C01737970}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StepWise.WebAPI.Tests", "test\StepWise.WebAPI.Tests\StepWise.WebAPI.Tests.csproj", "{DAE8E54E-0A43-4FD7-9D75-6081792FA94E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -47,6 +51,14 @@ Global {9BF63586-31E6-4075-B120-C8D2B6E7296A}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BF63586-31E6-4075-B120-C8D2B6E7296A}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BF63586-31E6-4075-B120-C8D2B6E7296A}.Release|Any CPU.Build.0 = Release|Any CPU + {8D2B6D19-3922-4B52-BB88-B28C01737970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D2B6D19-3922-4B52-BB88-B28C01737970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D2B6D19-3922-4B52-BB88-B28C01737970}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D2B6D19-3922-4B52-BB88-B28C01737970}.Release|Any CPU.Build.0 = Release|Any CPU + {DAE8E54E-0A43-4FD7-9D75-6081792FA94E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAE8E54E-0A43-4FD7-9D75-6081792FA94E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAE8E54E-0A43-4FD7-9D75-6081792FA94E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAE8E54E-0A43-4FD7-9D75-6081792FA94E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -56,6 +68,8 @@ Global {CEEC902A-E5F1-48DB-B549-D27C5011020A} = {0D861355-C022-4E1A-8C7B-7D1C3A066EE3} {2C1A5352-C488-4EBB-B3C6-2FDCE9B043F8} = {5E5C30E1-F538-430A-BE65-40C1C5B5C76A} {9BF63586-31E6-4075-B120-C8D2B6E7296A} = {0D861355-C022-4E1A-8C7B-7D1C3A066EE3} + {8D2B6D19-3922-4B52-BB88-B28C01737970} = {19750AFD-3091-4569-9D89-8D5735C3EBFC} + {DAE8E54E-0A43-4FD7-9D75-6081792FA94E} = {5E5C30E1-F538-430A-BE65-40C1C5B5C76A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55953F2E-2283-4F22-9B79-17E81B54BDCE} diff --git a/example/CodeInterpreter/Program.cs b/example/CodeInterpreter/Program.cs index 1166806..4933a35 100644 --- a/example/CodeInterpreter/Program.cs +++ b/example/CodeInterpreter/Program.cs @@ -41,10 +41,7 @@ var engine = StepWiseEngine.CreateFromInstance(codeInterpreter, maxConcurrency: 1, logger); var task = "use python to switch my system to dark mode"; -var input = new Dictionary -{ - ["task"] = StepVariable.Create(task), -}; +StepVariable[] input = [StepVariable.Create("task", task)]; await foreach (var stepResult in engine.ExecuteAsync(nameof(Workflow.GenerateReply), input)) { diff --git a/example/GetWeather/Program.cs b/example/GetWeather/Program.cs index b736fa5..d09aa8b 100644 --- a/example/GetWeather/Program.cs +++ b/example/GetWeather/Program.cs @@ -12,9 +12,10 @@ var getWeather = new Workflow(); var workflowEngine = StepWiseEngine.CreateFromInstance(getWeather, maxConcurrency: 3, loggerFactory.CreateLogger()); -var input = new Dictionary + +StepVariable[] input = new StepVariable[] { - { "cities", StepVariable.Create(new string[] { "Seattle", "Redmond" }) } + StepVariable.Create("cities", new string[] { "Seattle", "Redmond" }) }; await foreach (var stepResult in workflowEngine.ExecuteAsync(nameof(Workflow.GetWeatherAsync), input)) diff --git a/nuget/NUGET.md b/nuget/NUGET.md index 35c7d18..b9d8c3c 100644 --- a/nuget/NUGET.md +++ b/nuget/NUGET.md @@ -2,8 +2,4 @@ A powerful C# library for building and executing workflows. Define steps, manage Key features: • Intuitive step definition -• Automatic dependency resolution -• Attribute-based and programmatic workflow creation -• Flexible execution engine - -Simplify your workflow management with StepWise. \ No newline at end of file +• Automatic dependency resolution \ No newline at end of file diff --git a/nuget/icon.png b/nuget/icon.png index b7c0c7abd2ccf4a26ba01bfba6d8eeb7abdf519d..36130fd2d86b8b267f23810410bec1b2553f97d6 100644 GIT binary patch literal 5955 zcma)=Ra6vEyM|{drF*1H29QQNB?al8p@&vt=&m7^mKs7jq&uXB6cFi>mIj9qkrWUP z{^!3sSLb5y@8a8Quf4vt*LvUgiPqLsCd8w`0{{SouT&IuA1d3VDaz^lnje`31X3uq+&Qq3$jdS?EAmsvVF90Dv6@dRxjmVx;*e)! zp$66_2gDN_n(6fB5o?Ep;Z^d=xVlVoZDI+KlME0x9L6-@%`-9m#KU^0ZpFmJBu^&) zT+gSHT^dc0;^zsuJR9}APf`E4KU=P6p$W;6Aja4dV9gK1dWR=Om>h)#1B?JdIm!4D zB{5LKLYz7Rf-)8$CbZ(+923oakLrgmDe$(rE*wnEoR9U z;JIO;7Yq2_4gtqtrj^sklQ>HGq!dHiJQObuvt6KrkcSQDXFOgngkg*x&QK$p6iwe} zMaW2#CoL5{_0OFLu6B6Jy0eofOJq(7^hRJcD(jE&#x#z>g|giWe#X(h$bnbxSnZGFna?9Dsg=6RW($K%*34iC4IH0SA7yX6)+*V#%X`SOe8V0KblnlVbDCaIZ z@xI@_*5IoMwH@Yb!)3x|3iFoOLnE^hTv2B|Woqrr0op#r-pBdPfy;3|XM1d^$9L2w z_g-WcK|UDl2Mgjo6AJa9e5LWt&P0~4bz!|lWYE7(37HgUG6;gp97)WX)+7|ax1ttM z+dr79N5uI>Li}>D!y_BdM?k;x{MB~#iPK+0Ci=jUJQtwFp<_vGs6@77>M{x=rEna_#gw3!CmAN@uIG^-I3B-u-aEtBNyGdrKn7la@t>2OpK~)j%7Mi|-C`UYF4*F@EaR z!S+g8skrp$o^kk{1+3iJ{sfY>aC7%4y&M{|_IDJXZ>8DmA5*SdU;R5RJ`ea9ZJa|T z3`9QC_oSDd`JBni-AOecrxIN`@Xfy=&-q1^xZ0&^6u23XnUCG7|HyFUzy(!SF2PE| z-R@KO)6BRDC)9XLU_N`XHYo9<9Vd~Fg4wozvqfwLMy&HJxq40fW^PRAeu+_dlC;WN zu&x7-d+`tCu}t)%^d=xgsc~6KbBqw>(DOTkf<`7K_s+cSo=ibc>9Lu*$Ljsp?xk!H zxn6ROℑ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 ExecuteAsync( this IStepWiseEngine engine, string targetStepName, - Dictionary? inputs = null, + IEnumerable? inputs = null, bool earlyStop = true, int? maxSteps = null, CancellationToken ct = default) @@ -57,7 +57,7 @@ public static async Task ExecuteAsync( public static async IAsyncEnumerable ExecuteAsync( this IStepWiseEngine engine, string targetStepName, - Dictionary? inputs = null, + IEnumerable? inputs = null, bool earlyStop = true, int? maxSteps = null, [EnumeratorCancellation] diff --git a/src/StepWise.Core/IStepWiseEngine.cs b/src/StepWise.Core/IStepWiseEngine.cs index 7d09899..78a2ad4 100644 --- a/src/StepWise.Core/IStepWiseEngine.cs +++ b/src/StepWise.Core/IStepWiseEngine.cs @@ -6,12 +6,11 @@ namespace StepWise.Core; public interface IStepWiseEngine { /// - /// Execute the workflow until the target step is reached or no further steps can be executed. - /// If the is true, the workflow will stop as soon as the target step is reached and completed. - /// Otherwise, the workflow will continue to execute until no further steps can be executed. + /// Execute the workflow until the stop strategy is satisfied or no further steps can be executed. + /// IAsyncEnumerable ExecuteAsync( string targetStep, - Dictionary? inputs = null, + IEnumerable? inputs = null, IStepWiseEngineStopStrategy? stopStrategy = null, CancellationToken ct = default); } diff --git a/src/StepWise.Core/Step.cs b/src/StepWise.Core/Step.cs index 505ceae..df502a6 100644 --- a/src/StepWise.Core/Step.cs +++ b/src/StepWise.Core/Step.cs @@ -7,13 +7,14 @@ namespace StepWise.Core; public class Step { - internal Step(string name, List inputParameters, Type outputType, List dependencies, Delegate stepMethod) + internal Step(string name, string description, List inputParameters, Type outputType, List dependencies, Delegate stepMethod) { Name = name; InputParameters = inputParameters; OutputType = outputType; Dependencies = dependencies; StepMethod = stepMethod; + Description = description; } public static Step CreateFromMethod(Delegate stepMethod) @@ -24,9 +25,9 @@ public static Step CreateFromMethod(Delegate stepMethod) var outputType = stepMethod.Method.ReturnType; // the outputType must be an awaitable type - if (!outputType.IsGenericType || outputType.GetGenericTypeDefinition() != typeof(Task<>)) + if ((!outputType.IsGenericType || outputType.GetGenericTypeDefinition() != typeof(Task<>)) && outputType != typeof(Task)) { - throw new ArgumentException("The return type of the step method must be Task."); + throw new ArgumentException("The return type of the step method must be Task<> or Task."); } // get the input parameters @@ -51,7 +52,7 @@ public static Step CreateFromMethod(Delegate stepMethod) inputParameters.Add(new Parameter(param.Name!, param.ParameterType, sourceStep, hasDefaultValue, param.DefaultValue)); } - return new Step(name, inputParameters, outputType, dependencies, stepMethod); + return new Step(name, string.Empty, inputParameters, outputType, dependencies, stepMethod); } public string Name { get; set; } @@ -60,6 +61,8 @@ public static Step CreateFromMethod(Delegate stepMethod) public List Dependencies { get; set; } public Delegate StepMethod { get; set; } + public string Description { get; set; } + public bool IsExecuctionConditionSatisfied(Dictionary inputs) { foreach (var param in InputParameters) @@ -149,21 +152,24 @@ public override string ToString() public class StepVariable { - public StepVariable(int generation, object value) + public StepVariable(string name, int generation, object value) { Generation = generation; Value = value; + Name = name; } - public static StepVariable Create(object value, int generation = 0) + public static StepVariable Create(string name, object value, int generation = 0) { - return new StepVariable(generation, value); + return new StepVariable(name, generation, value); } public int Generation { get; set; } public object Value { get; set; } + public string Name { get; set; } + /// /// A convenient method to cast the result to the specified type. /// @@ -192,6 +198,8 @@ private StepRun(Step step, int generation, Dictionary inpu public Step Step => _step; + public string StepName => _step.Name; + public int Generation => _generation; public Dictionary Inputs => _inputs; diff --git a/src/StepWise.Core/StepWise.Core.csproj b/src/StepWise.Core/StepWise.Core.csproj index 5311e3b..96e5e5e 100644 --- a/src/StepWise.Core/StepWise.Core.csproj +++ b/src/StepWise.Core/StepWise.Core.csproj @@ -1,11 +1,15 @@  + LittleLittleCloud.StepWise.Core + StepWise.Core $(StepWiseTargetFrameworks) enable enable + + diff --git a/src/StepWise.Core/StepWiseEngine.cs b/src/StepWise.Core/StepWiseEngine.cs index 1af8c2f..5f573bd 100644 --- a/src/StepWise.Core/StepWiseEngine.cs +++ b/src/StepWise.Core/StepWiseEngine.cs @@ -31,12 +31,12 @@ public static StepWiseEngine CreateFromInstance(object instance, int maxConcurre public async IAsyncEnumerable ExecuteAsync( string targetStep, - Dictionary? inputs = null, + IEnumerable? inputs = null, IStepWiseEngineStopStrategy? stopStrategy = null, [EnumeratorCancellation] CancellationToken ct = default) { - inputs ??= new Dictionary(); + inputs ??= []; stopStrategy ??= new NeverStopStopStrategy(); this._logger?.LogInformation($"Starting the workflow engine with target step '{targetStep}' and stop strategy '{stopStrategy.Name}'."); @@ -98,7 +98,7 @@ void DFS(Step step) private async IAsyncEnumerable ExecuteStepAsync( Step step, - Dictionary inputs, + IEnumerable inputs, [EnumeratorCancellation] CancellationToken ct = default) { @@ -110,7 +110,13 @@ private async IAsyncEnumerable ExecuteStepAsync( // add inputs to context foreach (var input in inputs) { - _context[input.Key] = input.Value; + if (_context.TryGetValue(input.Name, out var value) && value.Generation > input.Generation) + { + _logger?.LogInformation($"Skipping adding input '{input.Name}' to the context because a newer version already exists."); + continue; + } + + _context[input.Name] = input; } // produce initial steps @@ -288,7 +294,7 @@ private async Task ExecuteSingleStepAsync( { _logger?.LogInformation($"[Runner {runnerId}]: updating context with the result of {stepRun}."); _logger?.LogDebug($"[Runner {runnerId}]: {stepRun} result is '{res}'."); - var stepVariable = StepVariable.Create(res, stepRun.Generation); + var stepVariable = StepVariable.Create(stepRun.StepName, res, stepRun.Generation); _stepResultQueue.Add(StepRunAndResult.Create(stepRun, stepVariable)); } } diff --git a/src/StepWise.Core/Workflow.cs b/src/StepWise.Core/Workflow.cs index 79708d6..ba92021 100644 --- a/src/StepWise.Core/Workflow.cs +++ b/src/StepWise.Core/Workflow.cs @@ -11,9 +11,10 @@ public class Workflow private readonly Dictionary _steps = new(); private readonly List<(Step, Step)> _adajcentMap = new(); // from -> to - internal Workflow(Dictionary steps) + internal Workflow(string name, Dictionary steps) { _steps = steps; + Name = name; foreach (var step in steps.Values) { foreach (var dependency in step.Dependencies) @@ -25,9 +26,12 @@ internal Workflow(Dictionary steps) public Dictionary Steps => _steps; - public static Workflow CreateFromInstance(object instance) + public string Name { get; } + + public static Workflow CreateFromInstance(object instance, string? name = null) { var type = instance.GetType(); + name ??= type.Name; var steps = new Dictionary(); foreach (var method in type.GetMethods()) @@ -43,7 +47,7 @@ public static Workflow CreateFromInstance(object instance) steps.Add(step.Name, step); } - return new Workflow(steps); + return new Workflow(name, steps); } /// diff --git a/src/StepWise.WebAPI/DTO.cs b/src/StepWise.WebAPI/DTO.cs new file mode 100644 index 0000000..884974c --- /dev/null +++ b/src/StepWise.WebAPI/DTO.cs @@ -0,0 +1,55 @@ +// Copyright (c) LittleLittleCloud. All rights reserved. +// DTO.cs + +using System.Text.Json; +using StepWise.Core; + +namespace StepWise.WebAPI; + +public record VariableDTO(string Name, string Type, string DisplayValue, int Generation) +{ + public static VariableDTO FromVariable(StepVariable variable) + { + var typeString = variable.Value?.GetType().Name ?? "null"; + var displayValue = JsonSerializer.Serialize(variable.Value, new JsonSerializerOptions { WriteIndented = true }); + return new VariableDTO(variable.Name, typeString, displayValue, variable.Generation); + } +} +public record StepDTO(string Name, string? Description, string[]? Dependencies, string[]? Variables) +{ + public static StepDTO FromStep(Step step) + { + var dependencies = step.Dependencies.ToArray(); + var variables = step.InputParameters.Select(p => p.SourceStep ?? p.Name).ToArray(); + return new StepDTO(step.Name, step.Description, dependencies, variables); + } +} + +public record StepRunDTO(StepDTO Step, VariableDTO[] Variables, int Generation) +{ + public static StepRunDTO FromStepRun(StepRun stepRun) + { + var variables = stepRun.Inputs.Values.Select(VariableDTO.FromVariable).ToArray(); + return new StepRunDTO(StepDTO.FromStep(stepRun.Step), variables, stepRun.Generation); + + } +} + +public record StepRunAndResultDTO(StepRunDTO StepRun, VariableDTO? Result) +{ + public static StepRunAndResultDTO FromStepRunAndResult(StepRun stepRun, StepVariable? result = null) + { + var stepRunDTO = StepRunDTO.FromStepRun(stepRun); + var resultDTO = result is null ? null : VariableDTO.FromVariable(result); + return new StepRunAndResultDTO(stepRunDTO, resultDTO); + } +} + +public record WorkflowDTO(string Name, string? Description, StepDTO[] Steps) +{ + public static WorkflowDTO FromWorkflow(Workflow workflow) + { + var steps = workflow.Steps.Values.Select(StepDTO.FromStep).ToArray(); + return new WorkflowDTO(workflow.Name, null, steps); + } +} diff --git a/src/StepWise.WebAPI/Extension/HostBuilderExtension.cs b/src/StepWise.WebAPI/Extension/HostBuilderExtension.cs new file mode 100644 index 0000000..0386dd5 --- /dev/null +++ b/src/StepWise.WebAPI/Extension/HostBuilderExtension.cs @@ -0,0 +1,60 @@ +// Copyright (c) LittleLittleCloud. All rights reserved. +// HostBuilderExtension.cs + +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; + +namespace StepWise.WebAPI; + +public static class HostBuilderExtension +{ + public static IHostBuilder UseStepWiseServer(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureWebHost(webBuilder => + { + webBuilder.ConfigureServices(services => + { + services + .AddControllers() + .ConfigureApplicationPartManager(manager => + { + manager.FeatureProviders.Add(new StepWiseControllerV1Provider()); + }); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "StepWise Controller", Version = "v1" }); + }); + }); + + webBuilder.Configure((ctx, app) => + { + var env = ctx.HostingEnvironment; + if (env.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "StepWise Controller V1"); + }); + app.UseDeveloperExceptionPage(); + } + + // enable cors from the same origin + app.UseCors(builder => + { + builder.SetIsOriginAllowed(origin => new Uri(origin).Host == "localhost") + .AllowAnyHeader() + .AllowAnyMethod(); + }); + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + }); + }); + } +} \ No newline at end of file diff --git a/src/StepWise.WebAPI/StepWise.WebAPI.csproj b/src/StepWise.WebAPI/StepWise.WebAPI.csproj new file mode 100644 index 0000000..d90960a --- /dev/null +++ b/src/StepWise.WebAPI/StepWise.WebAPI.csproj @@ -0,0 +1,24 @@ + + + + LittleLittleCloud.StepWise.WebAPI + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/StepWise.WebAPI/StepWiseControllerV1.cs b/src/StepWise.WebAPI/StepWiseControllerV1.cs new file mode 100644 index 0000000..944d728 --- /dev/null +++ b/src/StepWise.WebAPI/StepWiseControllerV1.cs @@ -0,0 +1,96 @@ +// Copyright (c) LittleLittleCloud. All rights reserved. +// StepWiseControllerV1.cs + +using System.Collections.Concurrent; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.Logging; +using StepWise.Core; + +namespace StepWise.WebAPI; + +[ApiController] +[Route("api/v1/[controller]/[action]")] +internal class StepWiseControllerV1 : ControllerBase +{ + private readonly ILogger? _logger = null; + private readonly ConcurrentDictionary _workflows = new(); + + [HttpGet] + public IActionResult Get() + { + return Ok("Hello, StepWise!"); + } + + [HttpGet] + public async Task> Version() + { + _logger?.LogInformation("Getting version"); + + var assembly = Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + + // return major.minor.patch + + var versionString = $"{version!.Major}.{version.Minor}.{version.Build}"; + + return new OkObjectResult(versionString); + } + + [HttpGet] + public async Task> GetStep(string workflowName, string stepName) + { + if (!_workflows.TryGetValue(workflowName, out var workflow)) + { + return NotFound($"Workflow {workflowName} not found"); + } + + if (!workflow.Steps.TryGetValue(stepName, out var step)) + { + return NotFound($"Step {stepName} not found in workflow {workflowName}"); + } + + return Ok(StepDTO.FromStep(step)); + } + + [HttpGet] + public async Task> GetWorkflow(string workflowName) + { + if (!_workflows.TryGetValue(workflowName, out var workflow)) + { + return NotFound($"Workflow {workflowName} not found"); + } + + var steps = workflow.Steps.Values.Select(StepDTO.FromStep).ToArray(); + return Ok(WorkflowDTO.FromWorkflow(workflow)); + } + + [HttpGet] + public async Task> ListWorkflow() + { + return Ok(_workflows.Values.Select(WorkflowDTO.FromWorkflow).ToArray()); + } + + [HttpPost] + public async IAsyncEnumerable ExecuteStep(string workflow, string step) + { + var workflowInstance = _workflows[workflow]; + var stepInstance = workflowInstance.Steps[step]; + var engine = new StepWiseEngine(workflowInstance, logger: _logger); + + await foreach (var stepRunAndResult in engine.ExecuteAsync(step)) + { + yield return StepRunAndResultDTO.FromStepRunAndResult(stepRunAndResult.StepRun, stepRunAndResult.Result); + } + } +} + + +class StepWiseControllerV1Provider : ControllerFeatureProvider +{ + protected override bool IsController(TypeInfo typeInfo) + { + return typeof(StepWiseControllerV1).IsAssignableFrom(typeInfo); + } +} diff --git a/test/StepWise.Core.Tests/GuessNumberWorkflowTest.cs b/test/StepWise.Core.Tests/GuessNumberWorkflowTest.cs index 305ad5c..68b9347 100644 --- a/test/StepWise.Core.Tests/GuessNumberWorkflowTest.cs +++ b/test/StepWise.Core.Tests/GuessNumberWorkflowTest.cs @@ -85,11 +85,9 @@ public async Task ItGuessNumber() var workflow = Workflow.CreateFromInstance(this); var engine = new StepWiseEngine(workflow, logger: _logger); - var context = new Dictionary() - { - [nameof(InputNumber)] = StepVariable.Create(5) - }; - await foreach (var stepResult in engine.ExecuteAsync(nameof(FinalResult), context)) + var context = new Dictionary(); + StepVariable[] inputs = [StepVariable.Create(nameof(InputNumber), 5)]; + await foreach (var stepResult in engine.ExecuteAsync(nameof(FinalResult), inputs)) { var name = stepResult.StepName; var result = stepResult.Result; diff --git a/test/StepWise.Core.Tests/PrepareDinnerWorkflowTest.cs b/test/StepWise.Core.Tests/PrepareDinnerWorkflowTest.cs index c8682f8..9a3ba5d 100644 --- a/test/StepWise.Core.Tests/PrepareDinnerWorkflowTest.cs +++ b/test/StepWise.Core.Tests/PrepareDinnerWorkflowTest.cs @@ -68,10 +68,7 @@ public async Task ItPrepareDinnerConcurrentlyAsync() var engine = new StepWiseEngine(workflow, maxConcurrency: 10); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var result = await engine.ExecuteAsync(nameof(ServeDinner), new Dictionary - { - [nameof(ChopVegetables)] = StepVariable.Create(value), - }); + var result = await engine.ExecuteAsync(nameof(ServeDinner), [StepVariable.Create(nameof(ChopVegetables), value)]); stopwatch.Stop(); @@ -88,10 +85,7 @@ public async Task ItPrepareDinnerStepByStepAsync() var engine = new StepWiseEngine(workflow, maxConcurrency: 1); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var result = await engine.ExecuteAsync(nameof(ServeDinner), new Dictionary - { - [nameof(ChopVegetables)] = StepVariable.Create(new[] { "tomato", "onion", "garlic" }), - }); + var result = await engine.ExecuteAsync(nameof(ServeDinner), [StepVariable.Create(nameof(ChopVegetables), new string[] { "tomato", "onion", "garlic" })]); stopwatch.Stop(); diff --git a/test/StepWise.Core.Tests/StepAndWorkflowTests.cs b/test/StepWise.Core.Tests/StepAndWorkflowTests.cs index 58e0a07..c70e2ed 100644 --- a/test/StepWise.Core.Tests/StepAndWorkflowTests.cs +++ b/test/StepWise.Core.Tests/StepAndWorkflowTests.cs @@ -47,7 +47,7 @@ public async Task ItCreateWorkflow() } [Fact] - public async void ItReturnNullWhenParameterIsMissing() + public async Task ItReturnNullWhenParameterIsMissing() { var step = Step.CreateFromMethod(GetWeather); @@ -61,7 +61,7 @@ public async void ItReturnNullWhenParameterIsMissing() [Fact] - public async void ItCreateStepFromGetWeather() + public async Task ItCreateStepFromGetWeather() { var step = Step.CreateFromMethod(GetWeather); @@ -105,15 +105,16 @@ public async Task ItThrowExceptionWhenCreatingStepFromGetDate() // because the return type is not Task var act = () => Step.CreateFromMethod(GetDate); - act.Should().Throw().WithMessage("The return type of the step method must be Task."); + act.Should().Throw().WithMessage("The return type of the step method must be Task<> or Task."); } [Fact] - public async Task ItThrowExceptionWhenCreatingStepFromDoNothing() + public async Task ItCreatingStepFromDoNothing() { - // because the return type is not Task - var act = () => Step.CreateFromMethod(DoNothing); + var step = Step.CreateFromMethod(DoNothing); - act.Should().Throw().WithMessage("The return type of the step method must be Task."); + step.Name.Should().Be(nameof(DoNothing)); + step.InputParameters.Should().BeEmpty(); + step.OutputType.Should().Be(typeof(Task)); } } diff --git a/test/StepWise.WebAPI.Tests/ApprovalTests/StepWiseControllerV1Tests.TestSwagger.approved.txt b/test/StepWise.WebAPI.Tests/ApprovalTests/StepWiseControllerV1Tests.TestSwagger.approved.txt new file mode 100644 index 0000000..2a51eab --- /dev/null +++ b/test/StepWise.WebAPI.Tests/ApprovalTests/StepWiseControllerV1Tests.TestSwagger.approved.txt @@ -0,0 +1,9 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "StepWise Controller", + "version": "v1" + }, + "paths": { }, + "components": { } +} \ No newline at end of file diff --git a/test/StepWise.WebAPI.Tests/Approvals/StepWiseControllerV1Tests.TestSwagger.approved.txt b/test/StepWise.WebAPI.Tests/Approvals/StepWiseControllerV1Tests.TestSwagger.approved.txt new file mode 100644 index 0000000..a45bcf5 --- /dev/null +++ b/test/StepWise.WebAPI.Tests/Approvals/StepWiseControllerV1Tests.TestSwagger.approved.txt @@ -0,0 +1,332 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "StepWise Controller", + "version": "v1" + }, + "paths": { + "/api/v1/StepWiseControllerV1/Get": { + "get": { + "tags": [ + "StepWiseControllerV1" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/StepWiseControllerV1/Version": { + "get": { + "tags": [ + "StepWiseControllerV1" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/StepWiseControllerV1/GetStep": { + "get": { + "tags": [ + "StepWiseControllerV1" + ], + "parameters": [ + { + "name": "workflowName", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "stepName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/StepDTO" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/StepDTO" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/StepDTO" + } + } + } + } + } + } + }, + "/api/v1/StepWiseControllerV1/GetWorkflow": { + "get": { + "tags": [ + "StepWiseControllerV1" + ], + "parameters": [ + { + "name": "workflowName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/WorkflowDTO" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowDTO" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowDTO" + } + } + } + } + } + } + }, + "/api/v1/StepWiseControllerV1/ListWorkflow": { + "get": { + "tags": [ + "StepWiseControllerV1" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowDTO" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowDTO" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowDTO" + } + } + } + } + } + } + } + }, + "/api/v1/StepWiseControllerV1/ExecuteStep": { + "post": { + "tags": [ + "StepWiseControllerV1" + ], + "parameters": [ + { + "name": "workflow", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "step", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StepRunAndResultDTO" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StepRunAndResultDTO" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StepRunAndResultDTO" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "StepDTO": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "dependencies": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "variables": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "StepRunAndResultDTO": { + "type": "object", + "properties": { + "stepRun": { + "$ref": "#/components/schemas/StepRunDTO" + }, + "result": { + "$ref": "#/components/schemas/VariableDTO" + } + }, + "additionalProperties": false + }, + "StepRunDTO": { + "type": "object", + "properties": { + "step": { + "$ref": "#/components/schemas/StepDTO" + }, + "variables": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VariableDTO" + }, + "nullable": true + }, + "generation": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "VariableDTO": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "nullable": true + }, + "displayValue": { + "type": "string", + "nullable": true + }, + "generation": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "WorkflowDTO": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StepDTO" + }, + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/StepWise.WebAPI.Tests/StepWise.WebAPI.Tests.csproj b/test/StepWise.WebAPI.Tests/StepWise.WebAPI.Tests.csproj new file mode 100644 index 0000000..b9f52bb --- /dev/null +++ b/test/StepWise.WebAPI.Tests/StepWise.WebAPI.Tests.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + diff --git a/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.received.txt b/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.received.txt new file mode 100644 index 0000000..65833b5 --- /dev/null +++ b/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.received.txt @@ -0,0 +1 @@ +s diff --git a/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.verified.txt b/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.verified.txt new file mode 100644 index 0000000..2a51eab --- /dev/null +++ b/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.TestSwagger.verified.txt @@ -0,0 +1,9 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "StepWise Controller", + "version": "v1" + }, + "paths": { }, + "components": { } +} \ No newline at end of file diff --git a/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.cs b/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.cs new file mode 100644 index 0000000..eca412d --- /dev/null +++ b/test/StepWise.WebAPI.Tests/StepWiseControllerV1Tests.cs @@ -0,0 +1,52 @@ +// Copyright (c) LittleLittleCloud. All rights reserved. +// StepWiseControllerV1Tests.cs + +using ApprovalTests; +using ApprovalTests.Namers; +using ApprovalTests.Reporters; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace StepWise.WebAPI.Tests; + +public class StepWiseControllerV1Tests +{ + [Fact] + [UseReporter(typeof(DiffReporter))] + [UseApprovalSubdirectory("Approvals")] + public async Task TestSwagger() + { + using var host = await Host.CreateDefaultBuilder() + .UseEnvironment("Development") + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer() + .Configure(app => { }); + + }) + .UseStepWiseServer() + .StartAsync(); + + var server = host.GetTestServer(); + + using (var client = server.CreateClient()) + { + var source = Path.GetFullPath("Schema/StepWiseControllerV1.schema.json"); + var sourceContent = File.ReadAllText(source); + string result = await client.GetStringAsync("/swagger/v1/swagger.json"); + + Approvals.Verify(result); + Approvals.Verify(sourceContent); + + //var schemaFile = "chatroom_client_swagger_schema.json"; + //var schemaFilePath = Path.Join("Schema", schemaFile); + //var schemaJson = File.ReadAllText(schemaFilePath); + //var schema = JObject.Parse(schemaJson).ToString(); + //var resultJson = JObject.Parse(result).ToString(); + //resultJson.Should().BeEquivalentTo(schema); + } + } +}