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
23 changes: 5 additions & 18 deletions src/ProjGraph.Cli/Commands/ClassDiagramCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ProjGraph.Cli.Infrastructure;
using ProjGraph.Core.Models;
using ProjGraph.Lib.ClassDiagram.Application;
using ProjGraph.Lib.Core.Abstractions;
Expand All @@ -17,12 +18,12 @@ namespace ProjGraph.Cli.Commands;
/// <param name="analysisService">The class analysis service used to analyze C# files.</param>
/// <param name="mermaidRenderer">The diagram renderer for producing Mermaid class diagram output.</param>
/// <param name="console">The output console for writing results and errors.</param>
/// <param name="fileSystem">The file system abstraction for disk operations.</param>
/// <param name="outputWriter">The helper for writing rendered output to file or console.</param>
internal sealed class ClassDiagramCommand(
IClassAnalysisService analysisService,
IDiagramRenderer<ClassModel> mermaidRenderer,
IOutputConsole console,
IFileSystem fileSystem)
DiagramOutputWriter outputWriter)
: AsyncCommand<ClassDiagramCommand.Settings>
{
/// <summary>
Expand Down Expand Up @@ -149,26 +150,12 @@ public override async Task<int> ExecuteAsync(

var model = await analysisService.AnalyzeFileAsync(settings.Path, options);

var wrapInMarkdownFence = settings.Output?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is false;
var wrapInMarkdownFence = DiagramOutputWriter.ShouldWrapInMarkdownFence(settings.Output);

var mermaidOutput =
mermaidRenderer.Render(model, new DiagramOptions(settings.ShowTitle, wrapInMarkdownFence));

if (settings.Output is not null)
{
var directory = fileSystem.GetDirectoryName(settings.Output);
if (!string.IsNullOrEmpty(directory))
{
fileSystem.CreateDirectory(directory);
}

await fileSystem.WriteAllTextAsync(settings.Output, mermaidOutput, cancellationToken);
console.WriteInfo($"Saved to {settings.Output}");
}
else
{
console.WriteLine(mermaidOutput);
}
await outputWriter.WriteAsync(mermaidOutput, settings.Output, cancellationToken);

return 0;
}
Expand Down
23 changes: 5 additions & 18 deletions src/ProjGraph.Cli/Commands/ErdCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ProjGraph.Cli.Infrastructure;
using ProjGraph.Core.Exceptions;
using ProjGraph.Core.Models;
using ProjGraph.Lib.Core.Abstractions;
Expand All @@ -22,12 +23,12 @@ namespace ProjGraph.Cli.Commands;
/// <param name="efService">The Entity Framework analysis service for discovering and analyzing contexts and snapshots.</param>
/// <param name="mermaidRenderer">The diagram renderer for producing Mermaid ERD output.</param>
/// <param name="console">The output console for writing results and errors.</param>
/// <param name="fileSystem">The file system abstraction for disk operations.</param>
/// <param name="outputWriter">The helper for writing rendered output to file or console.</param>
internal sealed class ErdCommand(
IEfAnalysisService efService,
IDiagramRenderer<EfModel> mermaidRenderer,
IOutputConsole console,
IFileSystem fileSystem)
DiagramOutputWriter outputWriter)
: AsyncCommand<ErdCommand.Settings>
{
/// <summary>
Expand Down Expand Up @@ -130,26 +131,12 @@ public override async Task<int> ExecuteAsync(

var model = await AnalyzeModelAsync(targetPath, settings.ContextName, cancellationToken);

var wrapInMarkdownFence = settings.Output?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is false;
var wrapInMarkdownFence = DiagramOutputWriter.ShouldWrapInMarkdownFence(settings.Output);

var mermaidOutput =
mermaidRenderer.Render(model, new DiagramOptions(settings.ShowTitle, wrapInMarkdownFence));

if (settings.Output is not null)
{
var directory = fileSystem.GetDirectoryName(settings.Output);
if (!string.IsNullOrEmpty(directory))
{
fileSystem.CreateDirectory(directory);
}

await fileSystem.WriteAllTextAsync(settings.Output, mermaidOutput, cancellationToken);
console.WriteInfo($"Saved to {settings.Output}");
}
else
{
console.WriteLine(mermaidOutput);
}
await outputWriter.WriteAsync(mermaidOutput, settings.Output, cancellationToken);

return 0;
}
Expand Down
23 changes: 5 additions & 18 deletions src/ProjGraph.Cli/Commands/VisualizeCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ProjGraph.Cli.Infrastructure;
using ProjGraph.Core.Models;
using ProjGraph.Lib.Core.Abstractions;
using ProjGraph.Lib.ProjectGraph.Application;
Expand All @@ -21,12 +22,12 @@ namespace ProjGraph.Cli.Commands;
/// <param name="graphService">The graph service used to build the dependency graph.</param>
/// <param name="renderers">The collection of diagram renderers for different output formats.</param>
/// <param name="console">The output console for writing results and errors.</param>
/// <param name="fileSystem">The file system abstraction for disk operations.</param>
/// <param name="outputWriter">The helper for writing rendered output to file or console.</param>
internal sealed class VisualizeCommand(
IGraphService graphService,
IEnumerable<IDiagramRenderer<SolutionGraph>> renderers,
IOutputConsole console,
IFileSystem fileSystem)
DiagramOutputWriter outputWriter)
: AsyncCommand<VisualizeCommand.Settings>
{
private const string FormatMermaid = "mermaid";
Expand Down Expand Up @@ -155,26 +156,12 @@ await Task.Run(() => graphService.BuildGraph(settings.Path), cancellationToken),
graph = result;
}

var wrapInMarkdownFence = settings.Output?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is false;
var wrapInMarkdownFence = DiagramOutputWriter.ShouldWrapInMarkdownFence(settings.Output);

var rendered = GetRenderer(settings.NormalizedFormat)
.Render(graph, new DiagramOptions(settings.ShowTitle, wrapInMarkdownFence));

if (settings.Output is not null)
{
var directory = fileSystem.GetDirectoryName(settings.Output);
if (!string.IsNullOrEmpty(directory))
{
fileSystem.CreateDirectory(directory);
}

await fileSystem.WriteAllTextAsync(settings.Output, rendered, cancellationToken);
console.WriteInfo($"Saved to {settings.Output}");
}
else
{
console.WriteLine(rendered);
}
await outputWriter.WriteAsync(rendered, settings.Output, cancellationToken);

return 0;
}
Expand Down
50 changes: 50 additions & 0 deletions src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using ProjGraph.Lib.Core.Abstractions;

namespace ProjGraph.Cli.Infrastructure;

/// <summary>
/// Provides a reusable helper for writing rendered diagram output either to a file or to the console.
/// </summary>
/// <param name="console">The output console for writing results.</param>
/// <param name="fileSystem">The file system abstraction for disk operations.</param>
internal sealed class DiagramOutputWriter(IOutputConsole console, IFileSystem fileSystem)
{
/// <summary>
/// Writes the rendered diagram to the specified output file, or to the console if no file path is given.
/// </summary>
/// <param name="rendered">The rendered diagram string to write.</param>
/// <param name="outputPath">
/// The optional file path to write to. When <see langword="null"/>, the output is written to the console.
/// </param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
public async Task WriteAsync(string rendered, string? outputPath, CancellationToken cancellationToken)
{
if (outputPath is not null)
{
var directory = fileSystem.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(directory))
{
fileSystem.CreateDirectory(directory);
}

await fileSystem.WriteAllTextAsync(outputPath, rendered, cancellationToken);
console.WriteInfo($"Saved to {outputPath}");
}
else
{
console.WriteLine(rendered);
}
}

/// <summary>
/// Determines whether the rendered output should be wrapped in a Markdown fence,
/// based on the output file extension. Files ending in <c>.mmd</c> are not wrapped.
/// When writing to the console (no output path), the output is wrapped.
/// </summary>
/// <param name="outputPath">The optional output file path.</param>
/// <returns><see langword="true"/> if the output should be wrapped in a Markdown fence; otherwise, <see langword="false"/>.</returns>
public static bool ShouldWrapInMarkdownFence(string? outputPath)
{
return outputPath?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is not true;
}
}
32 changes: 20 additions & 12 deletions src/ProjGraph.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,43 @@ internal static class Program
{
public static int Main(string[] args)
{
const string packageDiagramCommandName = "visualize";
const string erdCommandName = "erd";
const string classDiagramCommandName = "classdiagram";

var services = new ServiceCollection();

// Register Library services
services.AddProjGraphLib();

// Register CLI-specific services
services.AddSingleton<DiagramOutputWriter>();
Comment thread
HandyS11 marked this conversation as resolved.

var registrar = new TypeRegistrar(services);
var app = new CommandApp(registrar);

app.Configure(config =>
{
config.SetApplicationName("projgraph");

config.AddCommand<VisualizeCommand>("visualize")
config.AddCommand<VisualizeCommand>(packageDiagramCommandName)
.WithDescription("Visualize the dependency graph of a solution or project")
.WithExample("visualize", "MySolution.sln")
.WithExample("visualize", "MySolution.sln", "--format", "tree")
.WithExample("visualize", "MySolution.slnx", "--output", "graph.mmd");
.WithExample(packageDiagramCommandName, "MySolution.sln")
.WithExample(packageDiagramCommandName, "MySolution.sln", "--format", "tree")
.WithExample(packageDiagramCommandName, "MySolution.slnx", "--output", "graph.mmd");

config.AddCommand<ErdCommand>("erd")
config.AddCommand<ErdCommand>(erdCommandName)
.WithDescription("Generate a Mermaid ERD for an Entity Framework Core DbContext")
.WithExample("erd", "Data/AppDbContext.cs")
.WithExample("erd", "Data/AppDbContext.cs", "--context", "BlogContext")
.WithExample("erd", "Data/AppDbContext.cs", "--output", "docs/erd.md");
.WithExample(erdCommandName, "Data/AppDbContext.cs")
.WithExample(erdCommandName, "Data/AppDbContext.cs", "--context", "BlogContext")
.WithExample(erdCommandName, "Data/AppDbContext.cs", "--output", "docs/erd.md");

config.AddCommand<ClassDiagramCommand>("classdiagram")
config.AddCommand<ClassDiagramCommand>(classDiagramCommandName)
.WithDescription("Generate a Mermaid Class Diagram for a C# file")
.WithExample("classdiagram", "Services/UserService.cs")
.WithExample("classdiagram", "Models/User.cs", "--inheritance", "--dependencies", "--depth", "10")
.WithExample("classdiagram", "Models/User.cs", "--output", "user-hierarchy.mmd");
.WithExample(classDiagramCommandName, "Services/UserService.cs")
.WithExample(classDiagramCommandName, "Models/User.cs", "--inheritance", "--dependencies", "--depth",
"10")
.WithExample(classDiagramCommandName, "Models/User.cs", "--output", "user-hierarchy.mmd");
});

return app.Run(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static CommandApp CreateApp()
{
var services = new ServiceCollection();
services.AddProjGraphLib();
services.AddSingleton<DiagramOutputWriter>();

var registrar = new TypeRegistrar(services);
var app = new CommandApp(registrar);
Expand Down
Loading