// 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 --namespace "); 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(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 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 expectedFiles) { const string marker = "// "; 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; }