diff --git a/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowWriter.cs b/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowWriter.cs index ce53fe02..f678542f 100644 --- a/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowWriter.cs +++ b/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowWriter.cs @@ -534,8 +534,8 @@ private void WriteCommandStep( continue; foreach (var input in githubManualTrigger.Inputs.Where(i => target - .RequiredParams - .Select(p => p.ArgName) + .Params + .Select(p => p.Param.ArgName) .Any(p => p == i.Name))) env[input.Name] = $"${{{{ parameters.{input.Name} }}}}"; } @@ -545,12 +545,12 @@ private void WriteCommandStep( $"$({buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName})"; var requiredSecrets = target - .RequiredParams - .Where(x => x.IsSecret) + .Params + .Where(x => x.Param.IsSecret) .Select(x => x) .ToArray(); - if (requiredSecrets.Any(x => x.IsSecret)) + if (requiredSecrets.Any(x => x.Param.IsSecret)) { foreach (var injectedSecret in workflow.Options.OfType()) { @@ -594,10 +594,10 @@ private void WriteCommandStep( .Options .Concat(workflowStep.Options) .OfType() - .FirstOrDefault(x => x.Value == requiredSecret.Name); + .FirstOrDefault(x => x.Value == requiredSecret.Param.Name); if (injectedSecret is not null) - env[requiredSecret.ArgName] = $"$({requiredSecret.ArgName.ToUpper().Replace('-', '_')})"; + env[requiredSecret.Param.ArgName] = $"$({requiredSecret.Param.ArgName.ToUpper().Replace('-', '_')})"; } var environmentInjections = workflow.Options.OfType(); diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowWriter.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowWriter.cs index 6ed79ae3..a3032315 100644 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowWriter.cs +++ b/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowWriter.cs @@ -487,8 +487,8 @@ private void WriteCommandStep( continue; foreach (var input in githubManualTrigger.Inputs.Where(i => target - .RequiredParams - .Select(p => p.ArgName) + .Params + .Select(p => p.Param.ArgName) .Any(p => p == i.Name))) env[input.Name] = $"${{{{ inputs.{input.Name} }}}}"; } @@ -498,12 +498,12 @@ private void WriteCommandStep( $"${{{{ needs.{consumedVariable.TargetName}.outputs.{buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName} }}}}"; var requiredSecrets = target - .RequiredParams - .Where(x => x.IsSecret) + .Params + .Where(x => x.Param.IsSecret) .Select(x => x) .ToArray(); - if (requiredSecrets.Any(x => x.IsSecret)) + if (requiredSecrets.Any(x => x.Param.IsSecret)) { foreach (var injectedSecret in workflow.Options.OfType()) { @@ -547,10 +547,10 @@ private void WriteCommandStep( .Options .Concat(workflowStep.Options) .OfType() - .FirstOrDefault(x => x.Value == requiredSecret.Name); + .FirstOrDefault(x => x.Value == requiredSecret.Param.Name); if (injectedSecret is not null) - env[requiredSecret.ArgName] = $"${{{{ secrets.{requiredSecret.ArgName.ToUpper().Replace('-', '_')} }}}}"; + env[requiredSecret.Param.ArgName] = $"${{{{ secrets.{requiredSecret.Param.ArgName.ToUpper().Replace('-', '_')} }}}}"; } var environmentInjections = workflow.Options.OfType(); diff --git a/DecSm.Atom.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs b/DecSm.Atom.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs index 4e299ab2..344b324e 100644 --- a/DecSm.Atom.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs +++ b/DecSm.Atom.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs @@ -56,11 +56,6 @@ private static void GenerateCode( GeneratePartial(context, classSymbol, classDeclarationSyntax); } - private static string SimpleName(string fullName) => - fullName - .Split('.') - .Last(); - private static void GeneratePartial( SourceProductionContext context, INamedTypeSymbol classSymbol, diff --git a/DecSm.Atom.Tests/BuildTests/Params/OptionalParamBuild.cs b/DecSm.Atom.Tests/BuildTests/Params/OptionalParamBuild.cs new file mode 100644 index 00000000..f65c4454 --- /dev/null +++ b/DecSm.Atom.Tests/BuildTests/Params/OptionalParamBuild.cs @@ -0,0 +1,35 @@ +namespace DecSm.Atom.Tests.BuildTests.Params; + +[BuildDefinition] +public partial class OptionalParamBuild : BuildDefinition, IOptionalParamTarget1 +{ + public string? ExecuteValue1 { get; set; } + + public string? ExecuteValue2 { get; set; } +} + +[TargetDefinition] +public partial interface IOptionalParamTarget1 +{ + [ParamDefinition("param-1", "Param 1")] + string? Param1 => GetParam(() => Param1); + + [ParamDefinition("param-2", "Param 2")] + string? Param2 => GetParam(() => Param2); + + string? ExecuteValue1 { get; set; } + + string? ExecuteValue2 { get; set; } + + Target OptionalParamTarget1 => + t => t + .RequiresParam(nameof(Param1)) + .UsesParam(nameof(Param2)) + .Executes(() => + { + ExecuteValue1 = Param1; + ExecuteValue2 = Param2; + + return Task.CompletedTask; + }); +} diff --git a/DecSm.Atom.Tests/BuildTests/Params/ParamTests.cs b/DecSm.Atom.Tests/BuildTests/Params/ParamTests.cs index 00611c4e..d38e985c 100644 --- a/DecSm.Atom.Tests/BuildTests/Params/ParamTests.cs +++ b/DecSm.Atom.Tests/BuildTests/Params/ParamTests.cs @@ -55,4 +55,29 @@ public void Param_WhenRequiredAndNotSupplied_StopsAndReturnsError() .ToString() .ShouldContain("Missing required parameter 'param-2' for target ParamTarget2"); } + + [Test] + public void Param_WhenOptionalAndNotSupplied_UsesDefaultValue() + { + // Arrange + var loggerProvider = new TestLoggerProvider(); + + var host = CreateTestHost(commandLineArgs: new(true, + [ + new CommandArg(nameof(IOptionalParamTarget1.OptionalParamTarget1)), + new ParamArg("param-1", nameof(IOptionalParamTarget1.Param1), "TestValue"), + ]), + configure: builder => builder.Logging.AddProvider(loggerProvider)); + + var build = (OptionalParamBuild)host.Services.GetRequiredService(); + + // Act + host.Run(); + + // Assert + TestContext.Out.WriteLine(loggerProvider.Logger.LogContent.ToString()); + + build.ExecuteValue1.ShouldBe("TestValue"); + build.ExecuteValue2.ShouldBeNull(); + } } diff --git a/DecSm.Atom.Tests/ClassTests/Build/BuildExecutorTests.cs b/DecSm.Atom.Tests/ClassTests/Build/BuildExecutorTests.cs index 3528c9a8..ad7d8b8f 100644 --- a/DecSm.Atom.Tests/ClassTests/Build/BuildExecutorTests.cs +++ b/DecSm.Atom.Tests/ClassTests/Build/BuildExecutorTests.cs @@ -85,7 +85,7 @@ public async Task Execute_WhenBuildIsValid_SucceedsAndLogs() return Task.CompletedTask; }, ], - RequiredParams = [], + Params = [], ConsumedArtifacts = [], ProducedArtifacts = [], ConsumedVariables = [], diff --git a/DecSm.Atom.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs b/DecSm.Atom.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs index 0b69e8a0..245d0962 100644 --- a/DecSm.Atom.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs +++ b/DecSm.Atom.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs @@ -111,9 +111,10 @@ public void RequiresParam_AddsRequiredParam() targetDefinition.RequiresParam(paramName); // Assert - targetDefinition.RequiredParams.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + targetDefinition.Params.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), x => x.Count.ShouldBe(1), x => x[0] + .Param .ShouldBe(paramName)); } diff --git a/DecSm.Atom.Tests/ClassTests/Build/Model/BuildModelTests.cs b/DecSm.Atom.Tests/ClassTests/Build/Model/BuildModelTests.cs index 923577b0..6c19c4b8 100644 --- a/DecSm.Atom.Tests/ClassTests/Build/Model/BuildModelTests.cs +++ b/DecSm.Atom.Tests/ClassTests/Build/Model/BuildModelTests.cs @@ -25,7 +25,7 @@ public void CurrentTarget_WhenNoTargets_ReturnsNull() new("TargetModel", null, false) { Tasks = [], - RequiredParams = [], + Params = [], ConsumedArtifacts = [], ProducedArtifacts = [], ConsumedVariables = [], diff --git a/DecSm.Atom.Tool/Model.cs b/DecSm.Atom.Tool/Model.cs index 5f2fd9d2..7c1106f4 100644 --- a/DecSm.Atom.Tool/Model.cs +++ b/DecSm.Atom.Tool/Model.cs @@ -2,7 +2,10 @@ internal static class Model { - public static readonly Argument RunArgs = new("runArgs"); + public static readonly Argument RunArgs = new("runArgs") + { + DefaultValueFactory = _ => [], + }; public static readonly Option ProjectOption = new("--project", "-p") { diff --git a/DecSm.Atom/Build/BuildExecutor.cs b/DecSm.Atom/Build/BuildExecutor.cs index aa9cfc50..d7a583f5 100644 --- a/DecSm.Atom/Build/BuildExecutor.cs +++ b/DecSm.Atom/Build/BuildExecutor.cs @@ -46,29 +46,29 @@ public async Task Execute(CancellationToken cancellationToken) private void ValidateTargetParameters(TargetModel target) { - foreach (var requiredParam in target.RequiredParams) + foreach (var requiredParam in target.Params.Where(x => x.Required)) { - var defaultValue = requiredParam.DefaultValue is { Length: > 0 } - ? requiredParam.DefaultValue + var defaultValue = requiredParam.Param.DefaultValue is { Length: > 0 } + ? requiredParam.Param.DefaultValue : null; string? value; - if (requiredParam.IsSecret) + if (requiredParam.Param.IsSecret) { - value = paramService.GetParam(requiredParam.Name, defaultValue, x => x); + value = paramService.GetParam(requiredParam.Param.Name, defaultValue, x => x); } else { using var _ = paramService.CreateNoCacheScope(); - value = paramService.GetParam(requiredParam.Name, defaultValue); + value = paramService.GetParam(requiredParam.Param.Name, defaultValue); } if (value is { Length: > 0 }) continue; logger.LogError("Missing required parameter '{ParamName}' for target {TargetDefinitionName}", - requiredParam.ArgName, + requiredParam.Param.ArgName, target.Name); buildModel.TargetStates[target].Status = TargetRunState.Failed; diff --git a/DecSm.Atom/Build/BuildResolver.cs b/DecSm.Atom/Build/BuildResolver.cs index 1bdcfe65..0ce04759 100644 --- a/DecSm.Atom/Build/BuildResolver.cs +++ b/DecSm.Atom/Build/BuildResolver.cs @@ -35,7 +35,7 @@ public BuildModel Resolve() .ApplyExtensions(buildDefinition)) .ToArray(); - // Duplicate target names could result in undefined behavior + // Duplicate target names could result in undefined behavior, // So we fail early if any are found // Note: This doesn't include overriden or extended targets var duplicateTargetNames = targetDefinitions @@ -61,7 +61,7 @@ public BuildModel Resolve() return new TargetModel(x.Name, x.Description, x.Hidden) { Tasks = x.Tasks, - RequiredParams = x.RequiredParams.ConvertAll(p => paramModels[p]), + Params = x.Params.ConvertAll(p => new UsedParam(paramModels[p.Param], p.Required)), ConsumedArtifacts = x.ConsumedArtifacts, ProducedArtifacts = x.ProducedArtifacts, ConsumedVariables = x.ConsumedVariables, diff --git a/DecSm.Atom/Build/Definition/DefinedParam.cs b/DecSm.Atom/Build/Definition/DefinedParam.cs new file mode 100644 index 00000000..6b76d612 --- /dev/null +++ b/DecSm.Atom/Build/Definition/DefinedParam.cs @@ -0,0 +1,3 @@ +namespace DecSm.Atom.Build.Definition; + +public sealed record DefinedParam(string Param, bool Required); diff --git a/DecSm.Atom/Build/Definition/TargetDefinition.cs b/DecSm.Atom/Build/Definition/TargetDefinition.cs index 7551ccd1..a4f07a3f 100644 --- a/DecSm.Atom/Build/Definition/TargetDefinition.cs +++ b/DecSm.Atom/Build/Definition/TargetDefinition.cs @@ -37,9 +37,9 @@ public sealed class TargetDefinition public List Dependencies { get; private set; } = []; /// - /// Names of parameters that must be provided when invoking the target. + /// Names of parameters that may/must be provided when invoking the target. /// - public List RequiredParams { get; private set; } = []; + public List Params { get; private set; } = []; /// /// Artifacts that must be produced by other targets before this target can be executed. @@ -108,7 +108,7 @@ internal TargetDefinition ApplyExtensions(IBuildDefinition buildDefinition) { Tasks.AddRange(targetToExtend.Tasks); Dependencies.AddRange(targetToExtend.Dependencies); - RequiredParams.AddRange(targetToExtend.RequiredParams); + Params.AddRange(targetToExtend.Params); ConsumedArtifacts.AddRange(targetToExtend.ConsumedArtifacts); ProducedArtifacts.AddRange(targetToExtend.ProducedArtifacts); ConsumedVariables.AddRange(targetToExtend.ConsumedVariables); @@ -126,9 +126,9 @@ internal TargetDefinition ApplyExtensions(IBuildDefinition buildDefinition) .Concat(Dependencies) .ToList(); - RequiredParams = targetToExtend - .RequiredParams - .Concat(RequiredParams) + Params = targetToExtend + .Params + .Concat(Params) .ToList(); ConsumedArtifacts = targetToExtend @@ -244,6 +244,18 @@ public TargetDefinition DependsOn(WorkflowTargetDefinition workflowTarget) return this; } + /// + /// Specifies that this target may use the provided parameters for execution. + /// + /// An array of parameter names that may be used by the target. + /// This target definition. + public TargetDefinition UsesParam(params IEnumerable paramNames) + { + Params.AddRange(paramNames.Select(x => new DefinedParam(x, false))); + + return this; + } + /// /// Specifies that this target requires the provided parameters to be defined for execution. /// @@ -251,7 +263,7 @@ public TargetDefinition DependsOn(WorkflowTargetDefinition workflowTarget) /// This target definition. public TargetDefinition RequiresParam(params IEnumerable paramNames) { - RequiredParams.AddRange(paramNames); + Params.AddRange(paramNames.Select(x => new DefinedParam(x, true))); return this; } diff --git a/DecSm.Atom/Build/Model/TargetModel.cs b/DecSm.Atom/Build/Model/TargetModel.cs index 460ced73..a8e28fba 100644 --- a/DecSm.Atom/Build/Model/TargetModel.cs +++ b/DecSm.Atom/Build/Model/TargetModel.cs @@ -16,9 +16,9 @@ public sealed record TargetModel(string Name, string? Description, bool IsHidden public required IReadOnlyList> Tasks { get; init; } /// - /// Represents the input parameters that are mandatory for the execution of the target. + /// Represents the input parameters that are used for the execution of the target. /// - public required IReadOnlyList RequiredParams { get; init; } + public required IReadOnlyList Params { get; init; } /// /// Artifacts that are consumed by the target during the build process. diff --git a/DecSm.Atom/Build/Model/UsedParam.cs b/DecSm.Atom/Build/Model/UsedParam.cs new file mode 100644 index 00000000..433089b6 --- /dev/null +++ b/DecSm.Atom/Build/Model/UsedParam.cs @@ -0,0 +1,3 @@ +namespace DecSm.Atom.Build.Model; + +public sealed record UsedParam(ParamModel Param, bool Required); diff --git a/DecSm.Atom/Help/HelpService.cs b/DecSm.Atom/Help/HelpService.cs index 4cdae388..6cc80e94 100644 --- a/DecSm.Atom/Help/HelpService.cs +++ b/DecSm.Atom/Help/HelpService.cs @@ -115,20 +115,21 @@ private void WriteCommand(TargetModel target) } var secrets = target - .RequiredParams - .Where(x => x.IsSecret) + .Params + .Where(x => x.Param.IsSecret) .ToList(); var optionalParams = target - .RequiredParams + .Params .Except(secrets) - .Where(x => x.DefaultValue is { Length: > 0 } || + .Where(x => x.Param.DefaultValue is { Length: > 0 } || config - .GetSection("Params")[x.ArgName] is { Length: > 0 }) + .GetSection("Params")[x.Param.ArgName] is { Length: > 0 } || + !x.Required) .ToList(); var requiredParams = target - .RequiredParams + .Params .Except(secrets) .Except(optionalParams) .ToList(); @@ -139,11 +140,11 @@ private void WriteCommand(TargetModel target) foreach (var requiredParam in requiredParams) { - var descriptionDisplay = requiredParam.Description is { Length: > 0 } - ? $"[dim] | {requiredParam.Description.EscapeMarkup()}[/]" + var descriptionDisplay = requiredParam.Param.Description is { Length: > 0 } + ? $"[dim] | {requiredParam.Param.Description.EscapeMarkup()}[/]" : string.Empty; - reqTree.AddNode($"--{requiredParam.ArgName.EscapeMarkup()}{descriptionDisplay}"); + reqTree.AddNode($"--{requiredParam.Param.ArgName.EscapeMarkup()}{descriptionDisplay}"); } } @@ -153,10 +154,10 @@ private void WriteCommand(TargetModel target) foreach (var optionalParam in optionalParams) { - var defaultValue = optionalParam.DefaultValue; + var defaultValue = optionalParam.Param.DefaultValue; var configuredValue = config - .GetSection("Params")[optionalParam.ArgName]; + .GetSection("Params")[optionalParam.Param.ArgName]; var defaultDisplay = (defaultValue, configuredValue) switch { @@ -169,11 +170,11 @@ private void WriteCommand(TargetModel target) _ => string.Empty, }; - var descriptionDisplay = optionalParam.Description is { Length: > 0 } - ? $"[dim] | {optionalParam.Description.EscapeMarkup()}[/]" + var descriptionDisplay = optionalParam.Param.Description is { Length: > 0 } + ? $"[dim] | {optionalParam.Param.Description.EscapeMarkup()}[/]" : string.Empty; - optTree.AddNode($"--{optionalParam.ArgName.EscapeMarkup()}{defaultDisplay}{descriptionDisplay}"); + optTree.AddNode($"--{optionalParam.Param.ArgName.EscapeMarkup()}{defaultDisplay}{descriptionDisplay}"); } } @@ -183,11 +184,11 @@ private void WriteCommand(TargetModel target) foreach (var secret in secrets) { - var descriptionDisplay = secret.Description is { Length: > 0 } - ? $"[dim] | {secret.Description.EscapeMarkup()}[/]" + var descriptionDisplay = secret.Param.Description is { Length: > 0 } + ? $"[dim] | {secret.Param.Description.EscapeMarkup()}[/]" : string.Empty; - secTree.AddNode($"--{secret.ArgName.EscapeMarkup()}{descriptionDisplay}"); + secTree.AddNode($"--{secret.Param.ArgName.EscapeMarkup()}{descriptionDisplay}"); } }