Skip to content

Commit 9374bfc

Browse files
thomhurstclaude
andauthored
refactor: Consolidate duplicated generator logic into GeneratorUtils (#1468)
* chore: Add .worktrees/ to .gitignore for parallel development 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: Consolidate duplicated generator logic into GeneratorUtils Move shared code generation logic from 4 generator files to GeneratorUtils.cs: - GenerateCliAttributeString(): CLI attribute generation (42 lines) - GenerateValidationAttributes(): Validation attribute generation (14 lines) - GenerateXmlDocumentation(): XML doc comment generation (10+ locations) - GenerateMethodNameFromCommandParts(): Full method name from command parts - GenerateMethodNameFromLastCommandPart(): Method name from last segment This reduces ~80 lines of duplicated code across: - OptionsClassGenerator.cs - GlobalOptionsBaseGenerator.cs - ServiceInterfaceGenerator.cs - ServiceImplementationGenerator.cs - SubDomainClassGenerator.cs Fixes #1456 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a39cee9 commit 9374bfc

6 files changed

Lines changed: 148 additions & 215 deletions

File tree

tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Globalization;
22
using System.Text;
33
using System.Text.RegularExpressions;
4+
using ModularPipelines.OptionsGenerator.Models;
45

56
namespace ModularPipelines.OptionsGenerator.Generators;
67

@@ -164,4 +165,130 @@ public static string EscapeIdentifier(string identifier)
164165

165166
return CSharpKeywords.Contains(identifier) ? $"@{identifier}" : identifier;
166167
}
168+
169+
/// <summary>
170+
/// Generates the CLI attribute string for an option definition.
171+
/// Used by both OptionsClassGenerator and GlobalOptionsBaseGenerator.
172+
/// </summary>
173+
/// <param name="option">The CLI option definition.</param>
174+
/// <returns>The attribute string (e.g., "CliFlag(\"--verbose\")" or "CliOption(\"--output\")").</returns>
175+
public static string GenerateCliAttributeString(CliOptionDefinition option)
176+
{
177+
if (option.IsFlag)
178+
{
179+
// Use CliFlag for boolean flags
180+
var parts = new List<string> { $"\"{option.SwitchName}\"" };
181+
182+
if (!string.IsNullOrEmpty(option.ShortForm))
183+
{
184+
parts.Add($"ShortForm = \"{option.ShortForm}\"");
185+
}
186+
187+
return $"CliFlag({string.Join(", ", parts)})";
188+
}
189+
190+
// Use CliOption for value options
191+
var optionParts = new List<string> { $"\"{option.SwitchName}\"" };
192+
193+
if (!string.IsNullOrEmpty(option.ShortForm))
194+
{
195+
optionParts.Add($"ShortForm = \"{option.ShortForm}\"");
196+
}
197+
198+
if (option.ValueSeparator == "=")
199+
{
200+
optionParts.Add("Format = OptionFormat.EqualsSeparated");
201+
}
202+
else if (option.ValueSeparator == ":")
203+
{
204+
optionParts.Add("Format = OptionFormat.ColonSeparated");
205+
}
206+
else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator))
207+
{
208+
optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\"");
209+
}
210+
211+
if (option.AcceptsMultipleValues)
212+
{
213+
optionParts.Add("AllowMultiple = true");
214+
}
215+
216+
return $"CliOption({string.Join(", ", optionParts)})";
217+
}
218+
219+
/// <summary>
220+
/// Generates validation attribute lines for an option with validation constraints.
221+
/// </summary>
222+
/// <param name="sb">The StringBuilder to append to.</param>
223+
/// <param name="constraints">The validation constraints.</param>
224+
/// <param name="indent">The indentation string (defaults to 4 spaces).</param>
225+
public static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints, string indent = " ")
226+
{
227+
ArgumentNullException.ThrowIfNull(sb);
228+
ArgumentNullException.ThrowIfNull(constraints);
229+
230+
if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue)
231+
{
232+
var min = constraints.MinValue ?? int.MinValue;
233+
var max = constraints.MaxValue ?? int.MaxValue;
234+
sb.AppendLine($"{indent}[Range({min}, {max})]");
235+
}
236+
237+
if (!string.IsNullOrEmpty(constraints.Pattern))
238+
{
239+
sb.AppendLine($"{indent}[RegularExpression(@\"{constraints.Pattern}\")]");
240+
}
241+
}
242+
243+
/// <summary>
244+
/// Generates XML documentation block for a description.
245+
/// </summary>
246+
/// <param name="sb">The StringBuilder to append to.</param>
247+
/// <param name="description">The description text.</param>
248+
/// <param name="indent">The indentation string (defaults to 4 spaces).</param>
249+
public static void GenerateXmlDocumentation(StringBuilder sb, string? description, string indent = " ")
250+
{
251+
ArgumentNullException.ThrowIfNull(sb);
252+
253+
if (!string.IsNullOrEmpty(description))
254+
{
255+
sb.AppendLine($"{indent}/// <summary>");
256+
sb.AppendLine($"{indent}/// {EscapeXmlComment(description)}");
257+
sb.AppendLine($"{indent}/// </summary>");
258+
}
259+
}
260+
261+
/// <summary>
262+
/// Generates a C# method name from CLI command parts.
263+
/// Converts command parts to PascalCase method name.
264+
/// E.g., ["container", "create"] -> "ContainerCreate", ["build-server"] -> "BuildServer"
265+
/// </summary>
266+
/// <param name="commandParts">The command parts array.</param>
267+
/// <returns>The method name in PascalCase.</returns>
268+
public static string GenerateMethodNameFromCommandParts(string[] commandParts)
269+
{
270+
return string.Join("", commandParts
271+
.SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries))
272+
.Select(p => char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : "")));
273+
}
274+
275+
/// <summary>
276+
/// Generates a C# method name from the last part of a CLI command.
277+
/// Used for sub-domain commands where the method name is derived from the last segment.
278+
/// E.g., ["network", "create"] uses "create" -> "Create"
279+
/// </summary>
280+
/// <param name="command">The CLI command definition.</param>
281+
/// <returns>The method name in PascalCase, or "Execute" if no command parts.</returns>
282+
public static string GenerateMethodNameFromLastCommandPart(CliCommandDefinition command)
283+
{
284+
if (command.CommandParts.Length == 0)
285+
{
286+
return "Execute";
287+
}
288+
289+
var lastPart = command.CommandParts[^1];
290+
var parts = lastPart.Split('-', StringSplitOptions.RemoveEmptyEntries);
291+
return string.Join("", parts.Select(p =>
292+
char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : "")));
293+
}
167294
}

tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -88,85 +88,19 @@ private static string GenerateBaseOptionsClass(CliToolDefinition tool)
8888
private static void GenerateProperty(StringBuilder sb, CliOptionDefinition option)
8989
{
9090
// XML documentation
91-
if (!string.IsNullOrEmpty(option.Description))
92-
{
93-
sb.AppendLine(" /// <summary>");
94-
sb.AppendLine($" /// {EscapeXmlComment(option.Description)}");
95-
sb.AppendLine(" /// </summary>");
96-
}
91+
GeneratorUtils.GenerateXmlDocumentation(sb, option.Description);
9792

9893
// Validation attributes
9994
if (option.ValidationConstraints is not null)
10095
{
101-
GenerateValidationAttributes(sb, option.ValidationConstraints);
96+
GeneratorUtils.GenerateValidationAttributes(sb, option.ValidationConstraints);
10297
}
10398

10499
// Command attribute
105-
var attribute = GetAttributeString(option);
100+
var attribute = GeneratorUtils.GenerateCliAttributeString(option);
106101
sb.AppendLine($" [{attribute}]");
107102

108103
// Property
109104
sb.AppendLine($" public virtual {option.CSharpType} {option.PropertyName} {{ get; set; }}");
110105
}
111-
112-
private static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints)
113-
{
114-
if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue)
115-
{
116-
var min = constraints.MinValue ?? int.MinValue;
117-
var max = constraints.MaxValue ?? int.MaxValue;
118-
sb.AppendLine($" [Range({min}, {max})]");
119-
}
120-
121-
if (!string.IsNullOrEmpty(constraints.Pattern))
122-
{
123-
sb.AppendLine($" [RegularExpression(@\"{constraints.Pattern}\")]");
124-
}
125-
}
126-
127-
private static string GetAttributeString(CliOptionDefinition option)
128-
{
129-
if (option.IsFlag)
130-
{
131-
// Use CliFlag for boolean flags
132-
var parts = new List<string> { $"\"{option.SwitchName}\"" };
133-
134-
if (!string.IsNullOrEmpty(option.ShortForm))
135-
{
136-
parts.Add($"ShortForm = \"{option.ShortForm}\"");
137-
}
138-
139-
return $"CliFlag({string.Join(", ", parts)})";
140-
}
141-
142-
// Use CliOption for value options
143-
var optionParts = new List<string> { $"\"{option.SwitchName}\"" };
144-
145-
if (!string.IsNullOrEmpty(option.ShortForm))
146-
{
147-
optionParts.Add($"ShortForm = \"{option.ShortForm}\"");
148-
}
149-
150-
if (option.ValueSeparator == "=")
151-
{
152-
optionParts.Add("Format = OptionFormat.EqualsSeparated");
153-
}
154-
else if (option.ValueSeparator == ":")
155-
{
156-
optionParts.Add("Format = OptionFormat.ColonSeparated");
157-
}
158-
else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator))
159-
{
160-
optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\"");
161-
}
162-
163-
if (option.AcceptsMultipleValues)
164-
{
165-
optionParts.Add("AllowMultiple = true");
166-
}
167-
168-
return $"CliOption({string.Join(", ", optionParts)})";
169-
}
170-
171-
private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text);
172106
}

tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs

Lines changed: 6 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,7 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool
6565
sb.AppendLine();
6666

6767
// XML documentation
68-
if (!string.IsNullOrEmpty(command.Description))
69-
{
70-
sb.AppendLine("/// <summary>");
71-
sb.AppendLine($"/// {EscapeXmlComment(command.Description)}");
72-
sb.AppendLine("/// </summary>");
73-
}
68+
GeneratorUtils.GenerateXmlDocumentation(sb, command.Description, "");
7469

7570
// Class attributes
7671
sb.AppendLine("[ExcludeFromCodeCoverage]");
@@ -133,7 +128,7 @@ private static void GenerateClassDeclaration(StringBuilder sb, CliCommandDefinit
133128

134129
foreach (var opt in requiredOptions)
135130
{
136-
var attr = GetAttributeString(opt);
131+
var attr = GeneratorUtils.GenerateCliAttributeString(opt);
137132
parameters.Add($" [property: {attr}] {opt.CSharpType.TrimEnd('?')} {opt.PropertyName}");
138133
existingNames.Add(opt.PropertyName);
139134
}
@@ -162,94 +157,25 @@ private static void GenerateClassDeclaration(StringBuilder sb, CliCommandDefinit
162157
private static void GenerateProperty(StringBuilder sb, CliOptionDefinition option)
163158
{
164159
// XML documentation
165-
if (!string.IsNullOrEmpty(option.Description))
166-
{
167-
sb.AppendLine(" /// <summary>");
168-
sb.AppendLine($" /// {EscapeXmlComment(option.Description)}");
169-
sb.AppendLine(" /// </summary>");
170-
}
160+
GeneratorUtils.GenerateXmlDocumentation(sb, option.Description);
171161

172162
// Validation attributes
173163
if (option.ValidationConstraints is not null)
174164
{
175-
GenerateValidationAttributes(sb, option.ValidationConstraints);
165+
GeneratorUtils.GenerateValidationAttributes(sb, option.ValidationConstraints);
176166
}
177167

178168
// Command attribute
179-
var attribute = GetAttributeString(option);
169+
var attribute = GeneratorUtils.GenerateCliAttributeString(option);
180170
sb.AppendLine($" [{attribute}]");
181171

182172
// Property
183173
sb.AppendLine($" public {option.CSharpType} {option.PropertyName} {{ get; set; }}");
184174
}
185175

186-
private static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints)
187-
{
188-
if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue)
189-
{
190-
var min = constraints.MinValue ?? int.MinValue;
191-
var max = constraints.MaxValue ?? int.MaxValue;
192-
sb.AppendLine($" [Range({min}, {max})]");
193-
}
194-
195-
if (!string.IsNullOrEmpty(constraints.Pattern))
196-
{
197-
sb.AppendLine($" [RegularExpression(@\"{constraints.Pattern}\")]");
198-
}
199-
}
200-
201-
private static string GetAttributeString(CliOptionDefinition option)
202-
{
203-
if (option.IsFlag)
204-
{
205-
// Use CliFlag for boolean flags
206-
var parts = new List<string> { $"\"{option.SwitchName}\"" };
207-
208-
if (!string.IsNullOrEmpty(option.ShortForm))
209-
{
210-
parts.Add($"ShortForm = \"{option.ShortForm}\"");
211-
}
212-
213-
return $"CliFlag({string.Join(", ", parts)})";
214-
}
215-
216-
// Use CliOption for value options
217-
var optionParts = new List<string> { $"\"{option.SwitchName}\"" };
218-
219-
if (!string.IsNullOrEmpty(option.ShortForm))
220-
{
221-
optionParts.Add($"ShortForm = \"{option.ShortForm}\"");
222-
}
223-
224-
if (option.ValueSeparator == "=")
225-
{
226-
optionParts.Add("Format = OptionFormat.EqualsSeparated");
227-
}
228-
else if (option.ValueSeparator == ":")
229-
{
230-
optionParts.Add("Format = OptionFormat.ColonSeparated");
231-
}
232-
else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator))
233-
{
234-
optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\"");
235-
}
236-
237-
if (option.AcceptsMultipleValues)
238-
{
239-
optionParts.Add("AllowMultiple = true");
240-
}
241-
242-
return $"CliOption({string.Join(", ", optionParts)})";
243-
}
244-
245176
private static void GeneratePositionalArgument(StringBuilder sb, CliPositionalArgument positional)
246177
{
247-
if (!string.IsNullOrEmpty(positional.Description))
248-
{
249-
sb.AppendLine(" /// <summary>");
250-
sb.AppendLine($" /// {EscapeXmlComment(positional.Description)}");
251-
sb.AppendLine(" /// </summary>");
252-
}
178+
GeneratorUtils.GenerateXmlDocumentation(sb, positional.Description);
253179

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

283209
return $"CliArgument({string.Join(", ", parts)})";
284210
}
285-
286-
private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text);
287211
}

tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ private static string GenerateMainServiceClass(CliToolDefinition tool)
138138
StringComparer.OrdinalIgnoreCase);
139139

140140
var nonCollidingRootCommands = rootCommands
141-
.Where(c => !subDomainNames.Contains(GenerateMethodName(c)))
141+
.Where(c => !subDomainNames.Contains(GeneratorUtils.GenerateMethodNameFromCommandParts(c.CommandParts)))
142142
.ToList();
143143

144144
if (nonCollidingRootCommands.Count > 0)
@@ -162,7 +162,7 @@ private static string GenerateMainServiceClass(CliToolDefinition tool)
162162

163163
private static void GenerateMethod(StringBuilder sb, CliCommandDefinition command, CliToolDefinition tool)
164164
{
165-
var methodName = GenerateMethodName(command);
165+
var methodName = GeneratorUtils.GenerateMethodNameFromCommandParts(command.CommandParts);
166166
var hasRequiredParams = command.RequiredOptions.Count > 0 ||
167167
command.PositionalArguments.Any(p => p.IsRequired);
168168

@@ -190,14 +190,4 @@ private static void GenerateMethod(StringBuilder sb, CliCommandDefinition comman
190190

191191
sb.AppendLine(" }");
192192
}
193-
194-
private static string GenerateMethodName(CliCommandDefinition command)
195-
{
196-
// Convert command parts to method name
197-
// e.g., ["container", "create"] -> "ContainerCreate"
198-
// Handle hyphens within parts (e.g., "build-server" -> "BuildServer")
199-
return string.Join("", command.CommandParts
200-
.SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries))
201-
.Select(p => char.ToUpperInvariant(p[0]) + p[1..].ToLowerInvariant()));
202-
}
203193
}

0 commit comments

Comments
 (0)