feat: implemented an executable operation for Nitride

This commit is contained in:
D. Moonfire 2023-08-03 00:52:30 -05:00
parent 77ed31f12b
commit 580e44bb00
15 changed files with 465 additions and 6 deletions

View file

@ -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

View file

@ -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;
/// <summary>
/// An operation that wraps around CliWrap to run an executable.
/// </summary>
[WithProperties]
public partial class ExecOperation : AsyncOperationBase
{
private readonly ILogger logger;
private readonly IValidator<ExecOperation> validator;
public ExecOperation(
ILogger logger,
IValidator<ExecOperation> validator)
{
this.logger = logger.ForContext<ExecOperation>();
this.validator = validator;
}
/// <summary>
/// Gets or sets the command associated with this operation.
/// </summary>
public Func<Command>? CreateCommand { get; set; }
/// <summary>
/// Gets or sets a callback to process the buffered output.
/// </summary>
/// <remarks>
/// This is mutually exclusive with OnResult.
/// </remarks>
public Func<BufferedCommandResult, IEnumerable<Entity>>? OnBufferedResult
{
get;
set;
}
/// <summary>
/// Gets or sets a callback to process the output.
/// </summary>
/// <remarks>
/// This is mutually exclusive with OnBufferedResult.
/// </remarks>
public Func<CommandResult, IEnumerable<Entity>>? OnResult { get; set; }
/// <inheritdoc />
public override async IAsyncEnumerable<Entity> RunAsync(
IAsyncEnumerable<Entity> 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<Entity> 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<Entity>? list = this.OnResult?.Invoke(result);
if (list == null)
{
yield break;
}
foreach (Entity item in list)
{
yield return item;
}
}
}
}

View file

@ -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-"

View file

@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
<Description>An extension to Nitride static site generator to read and write files.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="7.0.1" />
<PackageReference Include="CliWrap" Version="3.6.4" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="FluentValidation" Version="11.6.0" />
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MAB.DotIgnore" Version="3.0.2" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Zio" Version="0.16.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Generators\MfGames.Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,10 @@
namespace MfGames.Nitride.Exec.Setup;
public static class NitrideExecBuilderExtensions
{
public static NitrideBuilder UseFeeds(this NitrideBuilder builder)
{
return builder
.UseModule<NitrideExecModule>();
}
}

View file

@ -0,0 +1,14 @@
using Autofac;
namespace MfGames.Nitride.Exec.Setup;
public class NitrideExecModule : Module
{
/// <inheritdoc />
protected override void Load(ContainerBuilder builder)
{
// Add in the operators and validators.
builder.RegisterOperators(this);
builder.RegisterValidators(this);
}
}

View file

@ -0,0 +1,20 @@
using FluentValidation;
namespace MfGames.Nitride.Exec.Validators;
public class ExecOperationValidator : AbstractValidator<ExecOperation>
{
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);
}
}

View file

@ -0,0 +1,14 @@
using MfGames.Gallium;
namespace MfGames.Nitride;
/// <summary>
/// Contains common functionality useful for async Nitride operations.
/// </summary>
public abstract class AsyncOperationBase : IAsyncOperation
{
/// <inheritdoc />
public abstract IAsyncEnumerable<Entity> RunAsync(
IAsyncEnumerable<Entity> input,
CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,16 @@
using MfGames.Gallium;
namespace MfGames.Nitride;
public interface IAsyncOperation
{
/// <summary>
/// Runs the input entities through the operation and returns the results.
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns>A list of modified entities.</returns>
IAsyncEnumerable<Entity> RunAsync(
IAsyncEnumerable<Entity> input,
CancellationToken cancellationToken = default);
}

View file

@ -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<IOperation>())
.AsSelf();
builder
.RegisterAssemblyTypes(module.GetType().Assembly)
.Where(x => x.IsAssignableTo<IAsyncOperation>())
.AsSelf();
}
public static void RegisterValidators(
this ContainerBuilder builder,
Module module)
{
builder.RegisterAssemblyTypes(
module.GetType()
.Assembly)
builder
.RegisterAssemblyTypes(module.GetType().Assembly)
.AsClosedTypesOf(typeof(IValidator<>));
}
}

View file

@ -22,4 +22,22 @@ public static class NitrideOperationExtensions
{
return operation.Run(input, cancellationToken);
}
/// <summary>
/// Runs the given configured operation against the input and returns
/// the results.
/// </summary>
/// <param name="input">The entities to perform the operation against.</param>
/// <param name="operation">The operation to run.</param>
/// <param name="cancellationToken">The cancellation token of the request.</param>
/// <returns>The results of the operation.</returns>
public static IAsyncEnumerable<Entity> Run(
this IEnumerable<Entity> input,
IAsyncOperation operation,
CancellationToken cancellationToken = default)
{
IAsyncEnumerable<Entity> list = input.ToAsyncEnumerable();
return operation.RunAsync(list, cancellationToken);
}
}

View file

@ -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<Entity> { new Entity().Set("initial") };
ExecOperation op = context
.Resolve<ExecOperation>()
.WithCreateCommand(
() => Cli
.Wrap("echo")
.WithArguments("test one two three"));
List<Entity> results = await lists.Run(op).ToListAsync();
Assert.Equal(
new[] { "initial" },
results.Select(a => a.Get<string>()));
}
[Fact]
public async Task BufferedEchoTest()
{
using NitrideTestContext context = this.CreateContext();
var lists = new List<Entity> { new Entity().Set("initial") };
ExecOperation op = context
.Resolve<ExecOperation>()
.WithCreateCommand(
() => Cli
.Wrap("echo")
.WithArguments("test one two three"))
.WithOnBufferedResult(
result => new[]
{
new Entity().Set(result.StandardOutput.Trim()),
});
List<Entity> results = await lists.Run(op).ToListAsync();
Assert.Equal(
new[]
{
"initial",
"test one two three",
},
results.Select(a => a.Get<string>()));
}
[Fact]
public async Task PipelineEchoTest()
{
using NitrideTestContext context = this.CreateContext();
var lists = new List<Entity> { new Entity().Set("initial") };
StringBuilder buffer = new();
ExecOperation op = context
.Resolve<ExecOperation>()
.WithCreateCommand(
() => Cli
.Wrap("echo")
.WithArguments("test one two three")
.WithStandardOutputPipe(
PipeTarget.ToStringBuilder(buffer)));
List<Entity> results = await lists.Run(op).ToListAsync();
Assert.Equal(
new[] { "initial" },
results.Select(a => a.Get<string>()));
Assert.Equal("test one two three", buffer.ToString().Trim());
}
}

View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.125" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Nitride.Exec\MfGames.Nitride.Exec.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.Json\MfGames.Nitride.Json.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,13 @@
using MfGames.TestSetup;
using Xunit.Abstractions;
namespace MfGames.Nitride.Exec.Tests;
public abstract class NitrideExecTestBase : TestBase<NitrideExecTestContext>
{
protected NitrideExecTestBase(ITestOutputHelper output)
: base(output)
{
}
}

View file

@ -0,0 +1,16 @@
using Autofac;
using MfGames.Nitride.Exec.Setup;
using MfGames.Nitride.Tests;
namespace MfGames.Nitride.Exec.Tests;
public class NitrideExecTestContext : NitrideTestContext
{
/// <inheritdoc />
protected override void ConfigureContainer(ContainerBuilder builder)
{
base.ConfigureContainer(builder);
builder.RegisterModule<NitrideExecModule>();
}
}