diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index 77657ad05e..9c711c354f 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Text; using System.Text.RegularExpressions; +using ModularPipelines.OptionsGenerator.Models; namespace ModularPipelines.OptionsGenerator.Generators; @@ -164,4 +165,130 @@ public static string EscapeIdentifier(string identifier) return CSharpKeywords.Contains(identifier) ? $"@{identifier}" : identifier; } + + /// + /// Generates the CLI attribute string for an option definition. + /// Used by both OptionsClassGenerator and GlobalOptionsBaseGenerator. + /// + /// The CLI option definition. + /// The attribute string (e.g., "CliFlag(\"--verbose\")" or "CliOption(\"--output\")"). + public static string GenerateCliAttributeString(CliOptionDefinition option) + { + if (option.IsFlag) + { + // Use CliFlag for boolean flags + var parts = new List { $"\"{option.SwitchName}\"" }; + + if (!string.IsNullOrEmpty(option.ShortForm)) + { + parts.Add($"ShortForm = \"{option.ShortForm}\""); + } + + return $"CliFlag({string.Join(", ", parts)})"; + } + + // Use CliOption for value options + var optionParts = new List { $"\"{option.SwitchName}\"" }; + + if (!string.IsNullOrEmpty(option.ShortForm)) + { + optionParts.Add($"ShortForm = \"{option.ShortForm}\""); + } + + if (option.ValueSeparator == "=") + { + optionParts.Add("Format = OptionFormat.EqualsSeparated"); + } + else if (option.ValueSeparator == ":") + { + optionParts.Add("Format = OptionFormat.ColonSeparated"); + } + else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator)) + { + optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\""); + } + + if (option.AcceptsMultipleValues) + { + optionParts.Add("AllowMultiple = true"); + } + + return $"CliOption({string.Join(", ", optionParts)})"; + } + + /// + /// Generates validation attribute lines for an option with validation constraints. + /// + /// The StringBuilder to append to. + /// The validation constraints. + /// The indentation string (defaults to 4 spaces). + public static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints, string indent = " ") + { + ArgumentNullException.ThrowIfNull(sb); + ArgumentNullException.ThrowIfNull(constraints); + + if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue) + { + var min = constraints.MinValue ?? int.MinValue; + var max = constraints.MaxValue ?? int.MaxValue; + sb.AppendLine($"{indent}[Range({min}, {max})]"); + } + + if (!string.IsNullOrEmpty(constraints.Pattern)) + { + sb.AppendLine($"{indent}[RegularExpression(@\"{constraints.Pattern}\")]"); + } + } + + /// + /// Generates XML documentation block for a description. + /// + /// The StringBuilder to append to. + /// The description text. + /// The indentation string (defaults to 4 spaces). + public static void GenerateXmlDocumentation(StringBuilder sb, string? description, string indent = " ") + { + ArgumentNullException.ThrowIfNull(sb); + + if (!string.IsNullOrEmpty(description)) + { + sb.AppendLine($"{indent}/// "); + sb.AppendLine($"{indent}/// {EscapeXmlComment(description)}"); + sb.AppendLine($"{indent}/// "); + } + } + + /// + /// Generates a C# method name from CLI command parts. + /// Converts command parts to PascalCase method name. + /// E.g., ["container", "create"] -> "ContainerCreate", ["build-server"] -> "BuildServer" + /// + /// The command parts array. + /// The method name in PascalCase. + public static string GenerateMethodNameFromCommandParts(string[] commandParts) + { + return string.Join("", commandParts + .SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries)) + .Select(p => char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : ""))); + } + + /// + /// Generates a C# method name from the last part of a CLI command. + /// Used for sub-domain commands where the method name is derived from the last segment. + /// E.g., ["network", "create"] uses "create" -> "Create" + /// + /// The CLI command definition. + /// The method name in PascalCase, or "Execute" if no command parts. + public static string GenerateMethodNameFromLastCommandPart(CliCommandDefinition command) + { + if (command.CommandParts.Length == 0) + { + return "Execute"; + } + + var lastPart = command.CommandParts[^1]; + var parts = lastPart.Split('-', StringSplitOptions.RemoveEmptyEntries); + return string.Join("", parts.Select(p => + char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : ""))); + } } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs index 089b19088d..b6471e1b6b 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs @@ -88,85 +88,19 @@ private static string GenerateBaseOptionsClass(CliToolDefinition tool) private static void GenerateProperty(StringBuilder sb, CliOptionDefinition option) { // XML documentation - if (!string.IsNullOrEmpty(option.Description)) - { - sb.AppendLine(" /// "); - sb.AppendLine($" /// {EscapeXmlComment(option.Description)}"); - sb.AppendLine(" /// "); - } + GeneratorUtils.GenerateXmlDocumentation(sb, option.Description); // Validation attributes if (option.ValidationConstraints is not null) { - GenerateValidationAttributes(sb, option.ValidationConstraints); + GeneratorUtils.GenerateValidationAttributes(sb, option.ValidationConstraints); } // Command attribute - var attribute = GetAttributeString(option); + var attribute = GeneratorUtils.GenerateCliAttributeString(option); sb.AppendLine($" [{attribute}]"); // Property sb.AppendLine($" public virtual {option.CSharpType} {option.PropertyName} {{ get; set; }}"); } - - private static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints) - { - if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue) - { - var min = constraints.MinValue ?? int.MinValue; - var max = constraints.MaxValue ?? int.MaxValue; - sb.AppendLine($" [Range({min}, {max})]"); - } - - if (!string.IsNullOrEmpty(constraints.Pattern)) - { - sb.AppendLine($" [RegularExpression(@\"{constraints.Pattern}\")]"); - } - } - - private static string GetAttributeString(CliOptionDefinition option) - { - if (option.IsFlag) - { - // Use CliFlag for boolean flags - var parts = new List { $"\"{option.SwitchName}\"" }; - - if (!string.IsNullOrEmpty(option.ShortForm)) - { - parts.Add($"ShortForm = \"{option.ShortForm}\""); - } - - return $"CliFlag({string.Join(", ", parts)})"; - } - - // Use CliOption for value options - var optionParts = new List { $"\"{option.SwitchName}\"" }; - - if (!string.IsNullOrEmpty(option.ShortForm)) - { - optionParts.Add($"ShortForm = \"{option.ShortForm}\""); - } - - if (option.ValueSeparator == "=") - { - optionParts.Add("Format = OptionFormat.EqualsSeparated"); - } - else if (option.ValueSeparator == ":") - { - optionParts.Add("Format = OptionFormat.ColonSeparated"); - } - else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator)) - { - optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\""); - } - - if (option.AcceptsMultipleValues) - { - optionParts.Add("AllowMultiple = true"); - } - - return $"CliOption({string.Join(", ", optionParts)})"; - } - - private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text); } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs index f355cf22de..bf41c9dd3d 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs @@ -65,12 +65,7 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool sb.AppendLine(); // XML documentation - if (!string.IsNullOrEmpty(command.Description)) - { - sb.AppendLine("/// "); - sb.AppendLine($"/// {EscapeXmlComment(command.Description)}"); - sb.AppendLine("/// "); - } + GeneratorUtils.GenerateXmlDocumentation(sb, command.Description, ""); // Class attributes sb.AppendLine("[ExcludeFromCodeCoverage]"); @@ -133,7 +128,7 @@ private static void GenerateClassDeclaration(StringBuilder sb, CliCommandDefinit foreach (var opt in requiredOptions) { - var attr = GetAttributeString(opt); + var attr = GeneratorUtils.GenerateCliAttributeString(opt); parameters.Add($" [property: {attr}] {opt.CSharpType.TrimEnd('?')} {opt.PropertyName}"); existingNames.Add(opt.PropertyName); } @@ -162,94 +157,25 @@ private static void GenerateClassDeclaration(StringBuilder sb, CliCommandDefinit private static void GenerateProperty(StringBuilder sb, CliOptionDefinition option) { // XML documentation - if (!string.IsNullOrEmpty(option.Description)) - { - sb.AppendLine(" /// "); - sb.AppendLine($" /// {EscapeXmlComment(option.Description)}"); - sb.AppendLine(" /// "); - } + GeneratorUtils.GenerateXmlDocumentation(sb, option.Description); // Validation attributes if (option.ValidationConstraints is not null) { - GenerateValidationAttributes(sb, option.ValidationConstraints); + GeneratorUtils.GenerateValidationAttributes(sb, option.ValidationConstraints); } // Command attribute - var attribute = GetAttributeString(option); + var attribute = GeneratorUtils.GenerateCliAttributeString(option); sb.AppendLine($" [{attribute}]"); // Property sb.AppendLine($" public {option.CSharpType} {option.PropertyName} {{ get; set; }}"); } - private static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints) - { - if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue) - { - var min = constraints.MinValue ?? int.MinValue; - var max = constraints.MaxValue ?? int.MaxValue; - sb.AppendLine($" [Range({min}, {max})]"); - } - - if (!string.IsNullOrEmpty(constraints.Pattern)) - { - sb.AppendLine($" [RegularExpression(@\"{constraints.Pattern}\")]"); - } - } - - private static string GetAttributeString(CliOptionDefinition option) - { - if (option.IsFlag) - { - // Use CliFlag for boolean flags - var parts = new List { $"\"{option.SwitchName}\"" }; - - if (!string.IsNullOrEmpty(option.ShortForm)) - { - parts.Add($"ShortForm = \"{option.ShortForm}\""); - } - - return $"CliFlag({string.Join(", ", parts)})"; - } - - // Use CliOption for value options - var optionParts = new List { $"\"{option.SwitchName}\"" }; - - if (!string.IsNullOrEmpty(option.ShortForm)) - { - optionParts.Add($"ShortForm = \"{option.ShortForm}\""); - } - - if (option.ValueSeparator == "=") - { - optionParts.Add("Format = OptionFormat.EqualsSeparated"); - } - else if (option.ValueSeparator == ":") - { - optionParts.Add("Format = OptionFormat.ColonSeparated"); - } - else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator)) - { - optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\""); - } - - if (option.AcceptsMultipleValues) - { - optionParts.Add("AllowMultiple = true"); - } - - return $"CliOption({string.Join(", ", optionParts)})"; - } - private static void GeneratePositionalArgument(StringBuilder sb, CliPositionalArgument positional) { - if (!string.IsNullOrEmpty(positional.Description)) - { - sb.AppendLine(" /// "); - sb.AppendLine($" /// {EscapeXmlComment(positional.Description)}"); - sb.AppendLine(" /// "); - } + GeneratorUtils.GenerateXmlDocumentation(sb, positional.Description); var attrString = GetPositionalAttributeString(positional); sb.AppendLine($" [{attrString}]"); @@ -282,6 +208,4 @@ private static string GetPositionalAttributeString(CliPositionalArgument positio return $"CliArgument({string.Join(", ", parts)})"; } - - private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text); } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs index d28721eb75..c39c3467d5 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs @@ -138,7 +138,7 @@ private static string GenerateMainServiceClass(CliToolDefinition tool) StringComparer.OrdinalIgnoreCase); var nonCollidingRootCommands = rootCommands - .Where(c => !subDomainNames.Contains(GenerateMethodName(c))) + .Where(c => !subDomainNames.Contains(GeneratorUtils.GenerateMethodNameFromCommandParts(c.CommandParts))) .ToList(); if (nonCollidingRootCommands.Count > 0) @@ -162,7 +162,7 @@ private static string GenerateMainServiceClass(CliToolDefinition tool) private static void GenerateMethod(StringBuilder sb, CliCommandDefinition command, CliToolDefinition tool) { - var methodName = GenerateMethodName(command); + var methodName = GeneratorUtils.GenerateMethodNameFromCommandParts(command.CommandParts); var hasRequiredParams = command.RequiredOptions.Count > 0 || command.PositionalArguments.Any(p => p.IsRequired); @@ -190,14 +190,4 @@ private static void GenerateMethod(StringBuilder sb, CliCommandDefinition comman sb.AppendLine(" }"); } - - private static string GenerateMethodName(CliCommandDefinition command) - { - // Convert command parts to method name - // e.g., ["container", "create"] -> "ContainerCreate" - // Handle hyphens within parts (e.g., "build-server" -> "BuildServer") - return string.Join("", command.CommandParts - .SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries)) - .Select(p => char.ToUpperInvariant(p[0]) + p[1..].ToLowerInvariant())); - } } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs index f3f6a9a0ec..7fbddd5b57 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs @@ -82,7 +82,7 @@ private static string GenerateInterface(CliToolDefinition tool) .ToList(); var nonCollidingRootCommands = rootCommands - .Where(c => !subDomainNames.Contains(GenerateMethodName(c))) + .Where(c => !subDomainNames.Contains(GeneratorUtils.GenerateMethodNameFromCommandParts(c.CommandParts))) .ToList(); if (nonCollidingRootCommands.Count > 0) @@ -107,14 +107,12 @@ private static string GenerateInterface(CliToolDefinition tool) private static void GenerateMethodSignature(StringBuilder sb, CliCommandDefinition command) { // Generate method name from command parts - var methodName = GenerateMethodName(command); + var methodName = GeneratorUtils.GenerateMethodNameFromCommandParts(command.CommandParts); // Single method - users set LogSettings on options if they need custom logging if (!string.IsNullOrEmpty(command.Description)) { - sb.AppendLine(" /// "); - sb.AppendLine($" /// {EscapeXmlComment(command.Description)}"); - sb.AppendLine(" /// "); + GeneratorUtils.GenerateXmlDocumentation(sb, command.Description); sb.AppendLine(" /// The command options."); sb.AppendLine(" /// Cancellation token."); sb.AppendLine(" /// The command result."); @@ -122,16 +120,4 @@ private static void GenerateMethodSignature(StringBuilder sb, CliCommandDefiniti sb.AppendLine($" Task {methodName}({command.ClassName} options, CancellationToken cancellationToken = default);"); } - - private static string GenerateMethodName(CliCommandDefinition command) - { - // Convert command parts to method name - // e.g., ["container", "create"] -> "ContainerCreate" - // Handle hyphens within parts (e.g., "build-server" -> "BuildServer") - return string.Join("", command.CommandParts - .SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries)) - .Select(p => char.ToUpperInvariant(p[0]) + p[1..].ToLowerInvariant())); - } - - private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text); } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs index 6ec4912ef3..8aad71da0e 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs @@ -52,7 +52,7 @@ private static void GenerateFilesFromTree( var collidingCommands = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var command in node.Commands) { - var methodName = GetMethodNameFromCommand(command); + var methodName = GeneratorUtils.GenerateMethodNameFromLastCommandPart(command); var matchingChild = node.Children.Values .FirstOrDefault(c => c.PascalSegment.Equals(methodName, StringComparison.OrdinalIgnoreCase)); @@ -194,16 +194,10 @@ private static string GenerateNodeClass( private static void GenerateExecuteMethod(StringBuilder sb, CliCommandDefinition command) { // Single method - users set LogSettings on options if they need custom logging - sb.AppendLine(" /// "); - if (!string.IsNullOrEmpty(command.Description)) - { - sb.AppendLine($" /// {EscapeXmlComment(command.Description)}"); - } - else - { - sb.AppendLine(" /// Executes the parent command directly."); - } - sb.AppendLine(" /// "); + var description = !string.IsNullOrEmpty(command.Description) + ? command.Description + : "Executes the parent command directly."; + GeneratorUtils.GenerateXmlDocumentation(sb, description); sb.AppendLine(" /// The command options."); sb.AppendLine(" /// Cancellation token."); sb.AppendLine(" /// The command result."); @@ -217,14 +211,12 @@ private static void GenerateExecuteMethod(StringBuilder sb, CliCommandDefinition private static void GenerateMethod(StringBuilder sb, CliCommandDefinition command, CommandTreeNode node) { - var methodName = GetMethodName(command, node); + var methodName = GeneratorUtils.GenerateMethodNameFromLastCommandPart(command); // Single method - users set LogSettings on options if they need custom logging if (!string.IsNullOrEmpty(command.Description)) { - sb.AppendLine(" /// "); - sb.AppendLine($" /// {EscapeXmlComment(command.Description)}"); - sb.AppendLine(" /// "); + GeneratorUtils.GenerateXmlDocumentation(sb, command.Description); sb.AppendLine(" /// The command options."); sb.AppendLine(" /// Cancellation token."); sb.AppendLine(" /// The command result."); @@ -237,24 +229,4 @@ private static void GenerateMethod(StringBuilder sb, CliCommandDefinition comman sb.AppendLine(" return await _command.ExecuteCommandLineTool(options, cancellationToken);"); sb.AppendLine(" }"); } - - private static string GetMethodName(CliCommandDefinition command, CommandTreeNode node) - { - return GetMethodNameFromCommand(command); - } - - private static string GetMethodNameFromCommand(CliCommandDefinition command) - { - if (command.CommandParts.Length == 0) - { - return "Execute"; - } - - var lastPart = command.CommandParts[^1]; - var parts = lastPart.Split('-', StringSplitOptions.RemoveEmptyEntries); - return string.Join("", parts.Select(p => - char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : ""))); - } - - private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text); }