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();
+ }
+}