diff --git a/MfGames.sln b/MfGames.sln index def0e44..54d7f64 100644 --- a/MfGames.sln +++ b/MfGames.sln @@ -89,6 +89,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NitrideCopyFiles", "example EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NitridePipelines", "examples\NitridePipelines\NitridePipelines.csproj", "{B044CB47-0024-4338-A56B-DCC049E06DED}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Exec", "src\MfGames.Nitride.Exec\MfGames.Nitride.Exec.csproj", "{BB098435-51A4-42BD-A14B-DC385326A426}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Exec.Tests", "tests\MfGames.Nitride.Exec.Tests\MfGames.Nitride.Exec.Tests.csproj", "{B47F12D0-70A0-410F-A023-97437A2CEDC6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -582,6 +586,30 @@ Global {B044CB47-0024-4338-A56B-DCC049E06DED}.Release|x64.Build.0 = Release|Any CPU {B044CB47-0024-4338-A56B-DCC049E06DED}.Release|x86.ActiveCfg = Release|Any CPU {B044CB47-0024-4338-A56B-DCC049E06DED}.Release|x86.Build.0 = Release|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Debug|x64.Build.0 = Debug|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Debug|x86.Build.0 = Debug|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Release|Any CPU.Build.0 = Release|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Release|x64.ActiveCfg = Release|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Release|x64.Build.0 = Release|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Release|x86.ActiveCfg = Release|Any CPU + {BB098435-51A4-42BD-A14B-DC385326A426}.Release|x86.Build.0 = Release|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Debug|x64.ActiveCfg = Debug|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Debug|x64.Build.0 = Debug|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Debug|x86.ActiveCfg = Debug|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Debug|x86.Build.0 = Debug|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Release|Any CPU.Build.0 = Release|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Release|x64.ActiveCfg = Release|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Release|x64.Build.0 = Release|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Release|x86.ActiveCfg = Release|Any CPU + {B47F12D0-70A0-410F-A023-97437A2CEDC6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5253E2A6-9565-45AF-92EA-1BFD3A63AC23} = {9C845D9A-B359-43B3-AE9E-B84CE945AF21} @@ -624,5 +652,7 @@ Global {25457946-9CD0-498E-8B46-03C420CCF103} = {9C845D9A-B359-43B3-AE9E-B84CE945AF21} {1843ECA6-18FD-4CE3-BCD5-6B478C4F893D} = {F79B6838-B175-43A3-8C52-69A414CC1386} {B044CB47-0024-4338-A56B-DCC049E06DED} = {F79B6838-B175-43A3-8C52-69A414CC1386} + {BB098435-51A4-42BD-A14B-DC385326A426} = {9C845D9A-B359-43B3-AE9E-B84CE945AF21} + {B47F12D0-70A0-410F-A023-97437A2CEDC6} = {4CE102F8-5C70-4696-B85F-93BB10034918} EndGlobalSection EndGlobal diff --git a/src/MfGames.Nitride.Exec/ExecOperation.cs b/src/MfGames.Nitride.Exec/ExecOperation.cs new file mode 100644 index 0000000..332ba47 --- /dev/null +++ b/src/MfGames.Nitride.Exec/ExecOperation.cs @@ -0,0 +1,119 @@ +using System.Runtime.CompilerServices; + +using CliWrap; +using CliWrap.Buffered; + +using FluentValidation; + +using MfGames.Gallium; +using MfGames.Nitride.Generators; + +using Serilog; + +namespace MfGames.Nitride.Exec; + +/// +/// An operation that wraps around CliWrap to run an executable. +/// +[WithProperties] +public partial class ExecOperation : AsyncOperationBase +{ + private readonly ILogger logger; + + private readonly IValidator validator; + + public ExecOperation( + ILogger logger, + IValidator validator) + { + this.logger = logger.ForContext(); + this.validator = validator; + } + + /// + /// Gets or sets the command associated with this operation. + /// + public Func? CreateCommand { get; set; } + + /// + /// Gets or sets a callback to process the buffered output. + /// + /// + /// This is mutually exclusive with OnResult. + /// + public Func>? OnBufferedResult + { + get; + set; + } + + /// + /// Gets or sets a callback to process the output. + /// + /// + /// This is mutually exclusive with OnBufferedResult. + /// + public Func>? OnResult { get; set; } + + /// + public override async IAsyncEnumerable RunAsync( + IAsyncEnumerable input, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Make sure everything is validated. + await this.validator.ValidateAndThrowAsync(this, cancellationToken); + + // Drain the inputs. + await foreach (Entity item in input.WithCancellation(cancellationToken)) + { + yield return item; + } + + // Create the command from the input, then execute it as a buffered + // output if we have a buffered result callback, otherwise as a command + // result. + Command command = this.CreateCommand!(); + + if (this.OnBufferedResult != null) + { + BufferedCommandResult result = await command + .ExecuteBufferedAsync(cancellationToken); + + this.logger.Debug( + "Execute buffered: {Command} {Arguments} = {ExitCode}", + command.TargetFilePath, + command.Arguments, + result.ExitCode); + + IEnumerable list = this.OnBufferedResult(result); + + foreach (Entity item in list) + { + yield return item; + } + } + else + { + CommandResult result = await command + .ExecuteAsync(cancellationToken); + + this.logger.Debug( + "Execute: {Command} {Arguments} = {ExitCode}", + command.TargetFilePath, + command.Arguments, + result.ExitCode); + + IEnumerable? list = this.OnResult?.Invoke(result); + + if (list == null) + { + yield break; + } + + foreach (Entity item in list) + { + yield return item; + } + } + } +} diff --git a/src/MfGames.Nitride.Exec/GitVersion.yml b/src/MfGames.Nitride.Exec/GitVersion.yml new file mode 100644 index 0000000..be87411 --- /dev/null +++ b/src/MfGames.Nitride.Exec/GitVersion.yml @@ -0,0 +1,14 @@ +mode: Mainline +increment: Inherit +continuous-delivery-fallback-tag: ci + +major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)" +minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:" +patch-version-bump-message: "^(fix|perf|refactor|revert)(\\([\\w\\s-]*\\))?:" +no-bump-message: "^(build|chore|ci|docs|style|test)(\\([\\w\\s-]*\\))?:" + +assembly-versioning-scheme: MajorMinorPatch +assembly-file-versioning-scheme: MajorMinorPatch +assembly-informational-format: "{InformationalVersion}" + +tag-prefix: "MfGames.Nitride.Exec-" diff --git a/src/MfGames.Nitride.Exec/MfGames.Nitride.Exec.csproj b/src/MfGames.Nitride.Exec/MfGames.Nitride.Exec.csproj new file mode 100644 index 0000000..140fe0e --- /dev/null +++ b/src/MfGames.Nitride.Exec/MfGames.Nitride.Exec.csproj @@ -0,0 +1,48 @@ + + + + net6.0 + enable + enable + + Dylan Moonfire + Moonfire Games + https://src.mfgames.com/mfgames-cil/mfgames-cil + Git + cli + https://src.mfgames.com/mfgames-cil/mfgames-cil + MIT + An extension to Nitride static site generator to read and write files. + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + True + + + + + Analyzer + False + + + + diff --git a/src/MfGames.Nitride.Exec/Setup/NitrideExecBuilderExtensions.cs b/src/MfGames.Nitride.Exec/Setup/NitrideExecBuilderExtensions.cs new file mode 100644 index 0000000..a804e97 --- /dev/null +++ b/src/MfGames.Nitride.Exec/Setup/NitrideExecBuilderExtensions.cs @@ -0,0 +1,10 @@ +namespace MfGames.Nitride.Exec.Setup; + +public static class NitrideExecBuilderExtensions +{ + public static NitrideBuilder UseFeeds(this NitrideBuilder builder) + { + return builder + .UseModule(); + } +} diff --git a/src/MfGames.Nitride.Exec/Setup/NitrideExecModule.cs b/src/MfGames.Nitride.Exec/Setup/NitrideExecModule.cs new file mode 100644 index 0000000..c00a3da --- /dev/null +++ b/src/MfGames.Nitride.Exec/Setup/NitrideExecModule.cs @@ -0,0 +1,14 @@ +using Autofac; + +namespace MfGames.Nitride.Exec.Setup; + +public class NitrideExecModule : Module +{ + /// + protected override void Load(ContainerBuilder builder) + { + // Add in the operators and validators. + builder.RegisterOperators(this); + builder.RegisterValidators(this); + } +} diff --git a/src/MfGames.Nitride.Exec/Validators/ExecOperationValidator.cs b/src/MfGames.Nitride.Exec/Validators/ExecOperationValidator.cs new file mode 100644 index 0000000..6072b5b --- /dev/null +++ b/src/MfGames.Nitride.Exec/Validators/ExecOperationValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; + +namespace MfGames.Nitride.Exec.Validators; + +public class ExecOperationValidator : AbstractValidator +{ + public ExecOperationValidator() + { + this.RuleFor(x => x.CreateCommand) + .NotNull(); + + this.RuleFor(x => x.OnBufferedResult) + .Null() + .When(x => x.OnResult != null); + + this.RuleFor(x => x.OnResult) + .Null() + .When(x => x.OnBufferedResult != null); + } +} diff --git a/src/MfGames.Nitride/AsyncOperationBase.cs b/src/MfGames.Nitride/AsyncOperationBase.cs new file mode 100644 index 0000000..f38f71d --- /dev/null +++ b/src/MfGames.Nitride/AsyncOperationBase.cs @@ -0,0 +1,14 @@ +using MfGames.Gallium; + +namespace MfGames.Nitride; + +/// +/// Contains common functionality useful for async Nitride operations. +/// +public abstract class AsyncOperationBase : IAsyncOperation +{ + /// + public abstract IAsyncEnumerable RunAsync( + IAsyncEnumerable input, + CancellationToken cancellationToken = default); +} diff --git a/src/MfGames.Nitride/IAsyncOperation.cs b/src/MfGames.Nitride/IAsyncOperation.cs new file mode 100644 index 0000000..92aede8 --- /dev/null +++ b/src/MfGames.Nitride/IAsyncOperation.cs @@ -0,0 +1,16 @@ +using MfGames.Gallium; + +namespace MfGames.Nitride; + +public interface IAsyncOperation +{ + /// + /// Runs the input entities through the operation and returns the results. + /// + /// + /// + /// A list of modified entities. + IAsyncEnumerable RunAsync( + IAsyncEnumerable input, + CancellationToken cancellationToken = default); +} diff --git a/src/MfGames.Nitride/NitrideModuleExtensions.cs b/src/MfGames.Nitride/NitrideModuleExtensions.cs index 09525c8..9cafd77 100644 --- a/src/MfGames.Nitride/NitrideModuleExtensions.cs +++ b/src/MfGames.Nitride/NitrideModuleExtensions.cs @@ -10,20 +10,22 @@ public static class NitrideModuleExtensions this ContainerBuilder builder, Module module) { - builder.RegisterAssemblyTypes( - module.GetType() - .Assembly) + builder + .RegisterAssemblyTypes(module.GetType().Assembly) .Where(x => x.IsAssignableTo()) .AsSelf(); + builder + .RegisterAssemblyTypes(module.GetType().Assembly) + .Where(x => x.IsAssignableTo()) + .AsSelf(); } public static void RegisterValidators( this ContainerBuilder builder, Module module) { - builder.RegisterAssemblyTypes( - module.GetType() - .Assembly) + builder + .RegisterAssemblyTypes(module.GetType().Assembly) .AsClosedTypesOf(typeof(IValidator<>)); } } diff --git a/src/MfGames.Nitride/NitrideOperationExtensions.cs b/src/MfGames.Nitride/NitrideOperationExtensions.cs index b89a934..1813a52 100644 --- a/src/MfGames.Nitride/NitrideOperationExtensions.cs +++ b/src/MfGames.Nitride/NitrideOperationExtensions.cs @@ -22,4 +22,22 @@ public static class NitrideOperationExtensions { return operation.Run(input, cancellationToken); } + + /// + /// Runs the given configured operation against the input and returns + /// the results. + /// + /// The entities to perform the operation against. + /// The operation to run. + /// The cancellation token of the request. + /// The results of the operation. + public static IAsyncEnumerable Run( + this IEnumerable input, + IAsyncOperation operation, + CancellationToken cancellationToken = default) + { + IAsyncEnumerable list = input.ToAsyncEnumerable(); + + return operation.RunAsync(list, cancellationToken); + } } diff --git a/tests/MfGames.Nitride.Exec.Tests/ExecOperationTests.cs b/tests/MfGames.Nitride.Exec.Tests/ExecOperationTests.cs new file mode 100644 index 0000000..ffceda6 --- /dev/null +++ b/tests/MfGames.Nitride.Exec.Tests/ExecOperationTests.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using CliWrap; + +using MfGames.Gallium; +using MfGames.Nitride.Tests; + +using Xunit; +using Xunit.Abstractions; + +namespace MfGames.Nitride.Exec.Tests; + +public class ExecOperationTests : NitrideExecTestBase +{ + public ExecOperationTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public async Task BlindEchoTest() + { + using NitrideTestContext context = this.CreateContext(); + var lists = new List { new Entity().Set("initial") }; + + ExecOperation op = context + .Resolve() + .WithCreateCommand( + () => Cli + .Wrap("echo") + .WithArguments("test one two three")); + + List results = await lists.Run(op).ToListAsync(); + + Assert.Equal( + new[] { "initial" }, + results.Select(a => a.Get())); + } + + [Fact] + public async Task BufferedEchoTest() + { + using NitrideTestContext context = this.CreateContext(); + var lists = new List { new Entity().Set("initial") }; + + ExecOperation op = context + .Resolve() + .WithCreateCommand( + () => Cli + .Wrap("echo") + .WithArguments("test one two three")) + .WithOnBufferedResult( + result => new[] + { + new Entity().Set(result.StandardOutput.Trim()), + }); + + List results = await lists.Run(op).ToListAsync(); + + Assert.Equal( + new[] + { + "initial", + "test one two three", + }, + results.Select(a => a.Get())); + } + + [Fact] + public async Task PipelineEchoTest() + { + using NitrideTestContext context = this.CreateContext(); + var lists = new List { new Entity().Set("initial") }; + StringBuilder buffer = new(); + + ExecOperation op = context + .Resolve() + .WithCreateCommand( + () => Cli + .Wrap("echo") + .WithArguments("test one two three") + .WithStandardOutputPipe( + PipeTarget.ToStringBuilder(buffer))); + + List results = await lists.Run(op).ToListAsync(); + + Assert.Equal( + new[] { "initial" }, + results.Select(a => a.Get())); + + Assert.Equal("test one two three", buffer.ToString().Trim()); + } +} diff --git a/tests/MfGames.Nitride.Exec.Tests/MfGames.Nitride.Exec.Tests.csproj b/tests/MfGames.Nitride.Exec.Tests/MfGames.Nitride.Exec.Tests.csproj new file mode 100644 index 0000000..e0cef58 --- /dev/null +++ b/tests/MfGames.Nitride.Exec.Tests/MfGames.Nitride.Exec.Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/tests/MfGames.Nitride.Exec.Tests/NitrideExecTestBase.cs b/tests/MfGames.Nitride.Exec.Tests/NitrideExecTestBase.cs new file mode 100644 index 0000000..780a258 --- /dev/null +++ b/tests/MfGames.Nitride.Exec.Tests/NitrideExecTestBase.cs @@ -0,0 +1,13 @@ +using MfGames.TestSetup; + +using Xunit.Abstractions; + +namespace MfGames.Nitride.Exec.Tests; + +public abstract class NitrideExecTestBase : TestBase +{ + protected NitrideExecTestBase(ITestOutputHelper output) + : base(output) + { + } +} diff --git a/tests/MfGames.Nitride.Exec.Tests/NitrideExecTestContext.cs b/tests/MfGames.Nitride.Exec.Tests/NitrideExecTestContext.cs new file mode 100644 index 0000000..f91fe7a --- /dev/null +++ b/tests/MfGames.Nitride.Exec.Tests/NitrideExecTestContext.cs @@ -0,0 +1,16 @@ +using Autofac; + +using MfGames.Nitride.Exec.Setup; +using MfGames.Nitride.Tests; + +namespace MfGames.Nitride.Exec.Tests; + +public class NitrideExecTestContext : NitrideTestContext +{ + /// + protected override void ConfigureContainer(ContainerBuilder builder) + { + base.ConfigureContainer(builder); + builder.RegisterModule(); + } +}