Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using ModularPipelines.OptionsGenerator.Models;

namespace ModularPipelines.OptionsGenerator.Generators;

Expand Down Expand Up @@ -164,4 +165,130 @@ public static string EscapeIdentifier(string identifier)

return CSharpKeywords.Contains(identifier) ? $"@{identifier}" : identifier;
}

/// <summary>
/// Generates the CLI attribute string for an option definition.
/// Used by both OptionsClassGenerator and GlobalOptionsBaseGenerator.
/// </summary>
/// <param name="option">The CLI option definition.</param>
/// <returns>The attribute string (e.g., "CliFlag(\"--verbose\")" or "CliOption(\"--output\")").</returns>
public static string GenerateCliAttributeString(CliOptionDefinition option)
{
if (option.IsFlag)
{
// Use CliFlag for boolean flags
var parts = new List<string> { $"\"{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<string> { $"\"{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)})";
}

/// <summary>
/// Generates validation attribute lines for an option with validation constraints.
/// </summary>
/// <param name="sb">The StringBuilder to append to.</param>
/// <param name="constraints">The validation constraints.</param>
/// <param name="indent">The indentation string (defaults to 4 spaces).</param>
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}\")]");
}
}

/// <summary>
/// Generates XML documentation block for a description.
/// </summary>
/// <param name="sb">The StringBuilder to append to.</param>
/// <param name="description">The description text.</param>
/// <param name="indent">The indentation string (defaults to 4 spaces).</param>
public static void GenerateXmlDocumentation(StringBuilder sb, string? description, string indent = " ")
{
ArgumentNullException.ThrowIfNull(sb);

if (!string.IsNullOrEmpty(description))
{
sb.AppendLine($"{indent}/// <summary>");
sb.AppendLine($"{indent}/// {EscapeXmlComment(description)}");
sb.AppendLine($"{indent}/// </summary>");
}
}

/// <summary>
/// Generates a C# method name from CLI command parts.
/// Converts command parts to PascalCase method name.
/// E.g., ["container", "create"] -> "ContainerCreate", ["build-server"] -> "BuildServer"
/// </summary>
/// <param name="commandParts">The command parts array.</param>
/// <returns>The method name in PascalCase.</returns>
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() : "")));
}

/// <summary>
/// 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"
/// </summary>
/// <param name="command">The CLI command definition.</param>
/// <returns>The method name in PascalCase, or "Execute" if no command parts.</returns>
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() : "")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(" /// <summary>");
sb.AppendLine($" /// {EscapeXmlComment(option.Description)}");
sb.AppendLine(" /// </summary>");
}
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<string> { $"\"{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<string> { $"\"{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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool
sb.AppendLine();

// XML documentation
if (!string.IsNullOrEmpty(command.Description))
{
sb.AppendLine("/// <summary>");
sb.AppendLine($"/// {EscapeXmlComment(command.Description)}");
sb.AppendLine("/// </summary>");
}
GeneratorUtils.GenerateXmlDocumentation(sb, command.Description, "");

// Class attributes
sb.AppendLine("[ExcludeFromCodeCoverage]");
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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(" /// <summary>");
sb.AppendLine($" /// {EscapeXmlComment(option.Description)}");
sb.AppendLine(" /// </summary>");
}
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<string> { $"\"{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<string> { $"\"{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(" /// <summary>");
sb.AppendLine($" /// {EscapeXmlComment(positional.Description)}");
sb.AppendLine(" /// </summary>");
}
GeneratorUtils.GenerateXmlDocumentation(sb, positional.Description);

var attrString = GetPositionalAttributeString(positional);
sb.AppendLine($" [{attrString}]");
Expand Down Expand Up @@ -282,6 +208,4 @@ private static string GetPositionalAttributeString(CliPositionalArgument positio

return $"CliArgument({string.Join(", ", parts)})";
}

private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);

Expand Down Expand Up @@ -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()));
}
}
Loading
Loading