From cb6beb0420536a4c42d9d9b6488be16a81f42cf9 Mon Sep 17 00:00:00 2001 From: Vaceslav Ustinov Date: Wed, 7 Jan 2026 17:08:15 +0100 Subject: [PATCH 1/5] feat: add processing warnings with report generation (#69) Add comprehensive warning collection during template processing: - ProcessingWarning type with rich context (type, message, variable name, context) - IWarningCollector interface for collecting warnings during visitor traversal - Warnings for missing variables, missing loop collections, null loop collections - ProcessingResult.Warnings property and HasWarnings helper Add warning report generation: - GetWarningReport() returns MemoryStream with formatted .docx report - GetWarningReportBytes() returns byte array for convenience - Uses embedded template processed by Templify itself - WarningReportTemplateGenerator creates the embedded template Integrate into GUI: - Display warnings summary after template processing - "Generate Warning Report" button to save detailed report - Auto-opens generated report in default application Normalize loop behavior: - Null collections now treated same as missing (silent removal) - Previously null collections threw exception while missing were silently removed --- .vscode/settings.json | 3 + .../WarningReportTemplateGenerator.cs | 129 ++++++ .../Program.cs | 1 + .../ViewModels/MainWindowViewModel.cs | 74 +++- TriasDev.Templify.Gui/Views/MainWindow.axaml | 4 + .../ProcessingWarningsIntegrationTests.cs | 402 ++++++++++++++++++ .../Visitors/CompositeVisitorTests.cs | 2 +- .../Visitors/ConditionalVisitorTests.cs | 20 +- .../Visitors/LoopVisitorTests.cs | 59 ++- .../Visitors/PlaceholderVisitorTests.cs | 22 +- .../Core/DocumentTemplateProcessor.cs | 14 +- TriasDev.Templify/Core/IWarningCollector.cs | 43 ++ TriasDev.Templify/Core/ProcessingResult.cs | 48 ++- TriasDev.Templify/Core/ProcessingWarning.cs | 127 ++++++ .../Core/WarningReportGenerator.cs | 110 +++++ .../Resources/WarningReportTemplate.docx | Bin 0 -> 1596 bytes TriasDev.Templify/TriasDev.Templify.csproj | 4 + .../Visitors/ConditionalVisitor.cs | 4 +- TriasDev.Templify/Visitors/LoopVisitor.cs | 17 +- .../Visitors/PlaceholderVisitor.cs | 8 +- examples/outputs/warning-report-output.docx | Bin 0 -> 1587 bytes .../templates/warning-report-template.docx | Bin 0 -> 1596 bytes 22 files changed, 1038 insertions(+), 53 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 TriasDev.Templify.DocumentGenerator/Generators/WarningReportTemplateGenerator.cs create mode 100644 TriasDev.Templify.Tests/Integration/ProcessingWarningsIntegrationTests.cs create mode 100644 TriasDev.Templify/Core/IWarningCollector.cs create mode 100644 TriasDev.Templify/Core/ProcessingWarning.cs create mode 100644 TriasDev.Templify/Core/WarningReportGenerator.cs create mode 100644 TriasDev.Templify/Resources/WarningReportTemplate.docx create mode 100644 examples/outputs/warning-report-output.docx create mode 100644 examples/templates/warning-report-template.docx diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..865798a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["Templify", "Trias"] +} diff --git a/TriasDev.Templify.DocumentGenerator/Generators/WarningReportTemplateGenerator.cs b/TriasDev.Templify.DocumentGenerator/Generators/WarningReportTemplateGenerator.cs new file mode 100644 index 0000000..b908075 --- /dev/null +++ b/TriasDev.Templify.DocumentGenerator/Generators/WarningReportTemplateGenerator.cs @@ -0,0 +1,129 @@ +using DocumentFormat.OpenXml.Packaging; + +namespace TriasDev.Templify.DocumentGenerator.Generators; + +/// +/// Generates the warning report template used by Templify to render processing warnings. +/// This template is embedded in the main library as a resource. +/// +public class WarningReportTemplateGenerator : BaseExampleGenerator +{ + public override string Name => "warning-report"; + + public override string Description => "Warning report template for Templify processing warnings"; + + public override string GenerateTemplate(string outputDirectory) + { + var templatePath = Path.Combine(outputDirectory, $"{Name}-template.docx"); + + using (var doc = CreateDocument(templatePath)) + { + var body = doc.MainDocumentPart!.Document.Body!; + + // Title + AddParagraph(body, "Template Processing Warning Report", isBold: true); + AddEmptyParagraph(body); + + // Metadata + AddParagraph(body, "Generated: {{GeneratedAt}}"); + AddEmptyParagraph(body); + + // Summary section + AddParagraph(body, "Summary", isBold: true); + AddParagraph(body, "Total Warnings: {{TotalWarnings}}"); + AddEmptyParagraph(body); + + // Summary table + var summaryTable = CreateTable(2); + AddTableHeaderRow(summaryTable, "Warning Type", "Count"); + AddTableRow(summaryTable, "Missing Variables", "{{MissingVariableCount}}"); + AddTableRow(summaryTable, "Missing Collections", "{{MissingCollectionCount}}"); + AddTableRow(summaryTable, "Null Collections", "{{NullCollectionCount}}"); + body.AppendChild(summaryTable); + AddEmptyParagraph(body); + + // Missing Variables section (conditional) + AddParagraph(body, "{{#if HasMissingVariables}}"); + AddParagraph(body, "Missing Variables", isBold: true); + AddParagraph(body, "The following variables were referenced in the template but not found in the data:"); + AddEmptyParagraph(body); + + var missingVarsTable = CreateTable(2); + AddTableHeaderRow(missingVarsTable, "Variable Name", "Context"); + AddTableRow(missingVarsTable, "{{#foreach MissingVariables}}", ""); + AddTableRow(missingVarsTable, "{{VariableName}}", "{{Context}}"); + AddTableRow(missingVarsTable, "{{/foreach}}", ""); + body.AppendChild(missingVarsTable); + AddEmptyParagraph(body); + AddParagraph(body, "{{/if}}"); + + // Missing Collections section (conditional) + AddParagraph(body, "{{#if HasMissingCollections}}"); + AddParagraph(body, "Missing Loop Collections", isBold: true); + AddParagraph(body, "The following collections were referenced in loops but not found in the data:"); + AddEmptyParagraph(body); + + var missingCollTable = CreateTable(2); + AddTableHeaderRow(missingCollTable, "Collection Name", "Context"); + AddTableRow(missingCollTable, "{{#foreach MissingCollections}}", ""); + AddTableRow(missingCollTable, "{{VariableName}}", "{{Context}}"); + AddTableRow(missingCollTable, "{{/foreach}}", ""); + body.AppendChild(missingCollTable); + AddEmptyParagraph(body); + AddParagraph(body, "{{/if}}"); + + // Null Collections section (conditional) + AddParagraph(body, "{{#if HasNullCollections}}"); + AddParagraph(body, "Null Loop Collections", isBold: true); + AddParagraph(body, "The following collections were found but had null values:"); + AddEmptyParagraph(body); + + var nullCollTable = CreateTable(2); + AddTableHeaderRow(nullCollTable, "Collection Name", "Context"); + AddTableRow(nullCollTable, "{{#foreach NullCollections}}", ""); + AddTableRow(nullCollTable, "{{VariableName}}", "{{Context}}"); + AddTableRow(nullCollTable, "{{/foreach}}", ""); + body.AppendChild(nullCollTable); + AddEmptyParagraph(body); + AddParagraph(body, "{{/if}}"); + + // Footer + AddEmptyParagraph(body); + AddParagraph(body, "End of Warning Report"); + + doc.Save(); + } + + return templatePath; + } + + public override Dictionary GetSampleData() + { + // Sample data for testing the template + return new Dictionary + { + ["GeneratedAt"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), + ["TotalWarnings"] = 5, + ["MissingVariableCount"] = 2, + ["MissingCollectionCount"] = 2, + ["NullCollectionCount"] = 1, + ["HasMissingVariables"] = true, + ["HasMissingCollections"] = true, + ["HasNullCollections"] = true, + ["MissingVariables"] = new List> + { + new() { ["VariableName"] = "CustomerName", ["Context"] = "placeholder" }, + new() { ["VariableName"] = "Customer.Email", ["Context"] = "placeholder" } + }, + ["MissingCollections"] = new List> + { + new() { ["VariableName"] = "OrderItems", ["Context"] = "loop: OrderItems" }, + new() { ["VariableName"] = "Categories", ["Context"] = "loop: Categories" } + }, + ["NullCollections"] = new List> + { + new() { ["VariableName"] = "Products", ["Context"] = "loop: Products" } + } + }; + } +} diff --git a/TriasDev.Templify.DocumentGenerator/Program.cs b/TriasDev.Templify.DocumentGenerator/Program.cs index 19e42f4..79f6ad6 100644 --- a/TriasDev.Templify.DocumentGenerator/Program.cs +++ b/TriasDev.Templify.DocumentGenerator/Program.cs @@ -42,6 +42,7 @@ new HelloWorldGenerator(), new InvoiceGenerator(), new ConditionalGenerator(), + new WarningReportTemplateGenerator(), }; // Parse command line arguments diff --git a/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs b/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs index 32ef7d6..82693d7 100644 --- a/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs +++ b/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs @@ -56,6 +56,13 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private bool _enableHtmlEntityReplacement; + /// + /// Stores the last processing result to enable warning report generation. + /// + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(GenerateWarningReportCommand))] + private UiProcessingResult? _lastProcessingResult; + public MainWindowViewModel( ITemplifyService templifyService, IFileDialogService fileDialogService) @@ -191,23 +198,27 @@ private async Task ProcessTemplateAsync() EnableHtmlEntityReplacement, progressReporter); + // Store for warning report generation + LastProcessingResult = result; + if (result.Success) { Results.Add("✓ Template processed successfully!"); Results.Add($"✓ Made {result.Processing.ReplacementCount} replacements"); Results.Add($"✓ Output saved to: {result.OutputPath}"); - if (result.Validation.MissingVariables.Count > 0) + if (result.Processing.HasWarnings) { - Results.Add($"⚠ {result.Validation.MissingVariables.Count} missing variables:"); - foreach (string missing in result.Validation.MissingVariables.Take(5)) + Results.Add($"⚠ {result.Processing.Warnings.Count} processing warnings:"); + foreach (ProcessingWarning warning in result.Processing.Warnings.Take(5)) { - Results.Add($" - {missing}"); + Results.Add($" - {warning.Type}: {warning.VariableName}"); } - if (result.Validation.MissingVariables.Count > 5) + if (result.Processing.Warnings.Count > 5) { - Results.Add($" ... and {result.Validation.MissingVariables.Count - 5} more"); + Results.Add($" ... and {result.Processing.Warnings.Count - 5} more"); } + Results.Add(" (Use 'Generate Warning Report' for full details)"); } StatusMessage = "Processing complete"; @@ -269,8 +280,59 @@ private void Clear() Results.Clear(); StatusMessage = "Ready"; Progress = 0; + LastProcessingResult = null; } + [RelayCommand(CanExecute = nameof(CanGenerateWarningReport))] + private async Task GenerateWarningReportAsync() + { + if (LastProcessingResult == null || !LastProcessingResult.Processing.HasWarnings) + { + return; + } + + try + { + // Generate default filename based on template name + string defaultName = "warning-report.docx"; + if (!string.IsNullOrEmpty(TemplatePath)) + { + string templateName = Path.GetFileNameWithoutExtension(TemplatePath); + defaultName = $"{templateName}-warnings.docx"; + } + + // Ask user where to save + string? savePath = await _fileDialogService.SaveOutputFileAsync(defaultName); + if (string.IsNullOrEmpty(savePath)) + { + return; // User cancelled + } + + // Generate and save the report + byte[] reportBytes = LastProcessingResult.Processing.GetWarningReportBytes(); + await File.WriteAllBytesAsync(savePath, reportBytes); + + Results.Add($"✓ Warning report saved to: {savePath}"); + StatusMessage = "Warning report generated"; + + // Offer to open the report + Process.Start(new ProcessStartInfo + { + FileName = savePath, + UseShellExecute = true + }); + } + catch (Exception ex) + { + Results.Add($"✗ Failed to generate warning report: {ex.Message}"); + } + } + + private bool CanGenerateWarningReport() => + LastProcessingResult != null && + LastProcessingResult.Success && + LastProcessingResult.Processing.HasWarnings; + private void UpdateOutputPath() { if (string.IsNullOrEmpty(TemplatePath)) diff --git a/TriasDev.Templify.Gui/Views/MainWindow.axaml b/TriasDev.Templify.Gui/Views/MainWindow.axaml index a2a7baa..93972aa 100644 --- a/TriasDev.Templify.Gui/Views/MainWindow.axaml +++ b/TriasDev.Templify.Gui/Views/MainWindow.axaml @@ -112,6 +112,10 @@ +