products/shared/ValueObjectsGenerator/Program.cs
2026-05-21 18:40:01 +00:00

175 lines
5.6 KiB
C#

// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.ValueObjectsGenerator;
// Simple argument parsing — this is internal tooling, no framework needed
string? path = null;
string? ns = null;
for (var i = 0; i < args.Length; i++)
{
if (args[i] == "--path" && i + 1 < args.Length)
{
path = args[++i];
}
else if (args[i] == "--namespace" && i + 1 < args.Length)
{
ns = args[++i];
}
}
if (path is null || ns is null)
{
Console.Error.WriteLine("Usage: ValueObjectsGenerator --path <projectDir> --namespace <rootNamespace>");
return 1;
}
if (!Directory.Exists(path))
{
Console.Error.WriteLine($"Directory not found: {path}");
return 1;
}
try
{
return Run(path, ns);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
{
Console.Error.WriteLine($"ValueObjectsGenerator failed: {ex.Message}");
return 1;
}
static int Run(string path, string ns)
{
var valueObjects = FileScanner.Scan(path, ns);
var written = 0;
var skipped = 0;
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Generate per-type .g.cs files next to their source files
foreach (var vo in valueObjects)
{
var sourceDir = Path.GetDirectoryName(vo.SourceFilePath)!;
var outputPath = Path.Combine(sourceDir, $"{vo.TypeName}.g.cs");
var content = vo.Kind switch
{
ValueObjectKind.StringValue => StringValueCodeGenerator.Generate(vo),
ValueObjectKind.ValueOf => ValueOfCodeGenerator.Generate(vo),
_ => throw new InvalidOperationException($"Unknown kind: {vo.Kind}")
};
_ = expectedFiles.Add(Path.GetFullPath(outputPath));
if (WriteIfChanged(outputPath, content))
{
written++;
}
else
{
skipped++;
}
}
// Generate infrastructure files in {path}/Internal/ValueObjects/
var valueObjectsDir = Path.Combine(path, "Internal", "ValueObjects");
_ = Directory.CreateDirectory(valueObjectsDir);
TrackAndWrite(ref written, ref skipped, expectedFiles,
Path.Combine(valueObjectsDir, "IValueOf.g.cs"), InfrastructureCodeGenerator.GetInterfaceSource(ns));
TrackAndWrite(ref written, ref skipped, expectedFiles,
Path.Combine(valueObjectsDir, "ValueOfTypeConverter.g.cs"), InfrastructureCodeGenerator.GetTypeConverterSource(ns));
TrackAndWrite(ref written, ref skipped, expectedFiles,
Path.Combine(valueObjectsDir, "CharsetExtensions.g.cs"), InfrastructureCodeGenerator.GetCharSetsSource(ns));
// Clean up orphaned .g.cs files that were previously generated by this tool
var deleted = CleanOrphanedFiles(path, expectedFiles);
Console.WriteLine($"ValueObjectsGenerator: {valueObjects.Count} value objects — {written} files written, {skipped} unchanged, {deleted} orphans deleted.");
return 0;
}
static void TrackAndWrite(ref int written, ref int skipped, HashSet<string> expectedFiles, string filePath, string content)
{
_ = expectedFiles.Add(Path.GetFullPath(filePath));
if (WriteIfChanged(filePath, content))
{
written++;
}
else
{
skipped++;
}
}
// Deletes orphaned .g.cs files that were previously generated by this tool but are no longer expected.
// Only deletes files that contain the ValueObjectsGenerator marker comment.
static int CleanOrphanedFiles(string rootPath, HashSet<string> expectedFiles)
{
const string marker = "// <auto-generated by=\"ValueObjectsGenerator\"/>";
var deleted = 0;
foreach (var file in Directory.EnumerateFiles(rootPath, "*.g.cs", SearchOption.AllDirectories))
{
var fullPath = Path.GetFullPath(file);
// Skip files in obj/ and bin/ directories
var normalized = fullPath.Replace('\\', '/');
if (normalized.Contains("/obj/", StringComparison.Ordinal) ||
normalized.Contains("/bin/", StringComparison.Ordinal))
{
continue;
}
// Skip files we just generated
if (expectedFiles.Contains(fullPath))
{
continue;
}
// Only delete files that were generated by this tool (contain our marker)
string? firstLine;
using (var reader = new StreamReader(file))
{
firstLine = reader.ReadLine();
}
if (firstLine == marker)
{
File.Delete(file);
Console.WriteLine($" Deleted orphaned file: {file}");
deleted++;
}
}
return deleted;
}
// Writes content to path only if it differs from what's already there.
// Returns true if the file was written, false if unchanged or locked.
static bool WriteIfChanged(string filePath, string content)
{
if (File.Exists(filePath))
{
var existing = File.ReadAllText(filePath);
// Normalize line endings for comparison
if (existing.Replace("\r\n", "\n", StringComparison.Ordinal) == content.Replace("\r\n", "\n", StringComparison.Ordinal))
{
return false;
}
}
// Use FileShare.ReadWrite so IDE language servers holding a shared lock don't block us.
// FileMode.OpenOrCreate + SetLength(0) avoids the truncation that FileMode.Create requires
// (which needs exclusive access on Windows).
using var stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
stream.SetLength(0);
using var writer = new StreamWriter(stream, System.Text.Encoding.UTF8);
writer.Write(content);
return true;
}