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 b7c0c7a..36130fd 100644 Binary files a/nuget/icon.png and b/nuget/icon.png differ diff --git a/schema/StepWiseControllerV1.schema.json b/schema/StepWiseControllerV1.schema.json new file mode 100644 index 0000000..a45bcf5 --- /dev/null +++ b/schema/StepWiseControllerV1.schema.json @@ -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/src/StepWise.Core/Extension/StepWiseEngineExtension.cs b/src/StepWise.Core/Extension/StepWiseEngineExtension.cs index 0f0e792..5c78c8d 100644 --- a/src/StepWise.Core/Extension/StepWiseEngineExtension.cs +++ b/src/StepWise.Core/Extension/StepWiseEngineExtension.cs @@ -18,7 +18,7 @@ public static class StepWiseEngineExtension public static async Task 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); + } + } +}