|
| 1 | +namespace OfficeIMO.Excel { |
| 2 | + /// <summary> |
| 3 | + /// Workflow-level OfficeIMO.Excel operation categories covered by feature preflight checks. |
| 4 | + /// </summary> |
| 5 | + public enum ExcelPreflightCapability { |
| 6 | + /// <summary>Read worksheet data, tables, and typed rows from the workbook.</summary> |
| 7 | + ReadWorkbookData, |
| 8 | + |
| 9 | + /// <summary>Edit existing cell values while preserving package parts OfficeIMO does not fully author yet.</summary> |
| 10 | + EditCellValues, |
| 11 | + |
| 12 | + /// <summary>Perform structure-changing edits such as adding/removing sheets, tables, drawings, pivots, or relationships.</summary> |
| 13 | + EditWorkbookStructure, |
| 14 | + |
| 15 | + /// <summary>Use cached formula values for reads, export, or downstream reporting.</summary> |
| 16 | + UseCachedFormulaValues, |
| 17 | + |
| 18 | + /// <summary>Calculate workbook formulas through OfficeIMO's lightweight evaluator.</summary> |
| 19 | + CalculateFormulas, |
| 20 | + |
| 21 | + /// <summary>Bind data into an Excel template and save the generated workbook.</summary> |
| 22 | + BindTemplate, |
| 23 | + |
| 24 | + /// <summary>Export a report workbook through the first-party OfficeIMO Excel-to-PDF path.</summary> |
| 25 | + ExportPdfReport |
| 26 | + } |
| 27 | + |
| 28 | + public sealed partial class ExcelFeatureReport { |
| 29 | + /// <summary> |
| 30 | + /// True when OfficeIMO can attempt read-oriented workbook data operations. |
| 31 | + /// </summary> |
| 32 | + public bool CanReadWorkbookData => UnsupportedFeatures.Count == 0; |
| 33 | + |
| 34 | + /// <summary> |
| 35 | + /// True when OfficeIMO can attempt cell-value edits without known unsupported package features. |
| 36 | + /// </summary> |
| 37 | + public bool CanEditCellValues => UnsupportedFeatures.Count == 0; |
| 38 | + |
| 39 | + /// <summary> |
| 40 | + /// True when OfficeIMO can attempt structure-changing workbook edits without preserve-only or unsupported feature blockers. |
| 41 | + /// </summary> |
| 42 | + public bool CanEditWorkbookStructure => !HasAdvancedFeatures; |
| 43 | + |
| 44 | + /// <summary> |
| 45 | + /// True when cached formula values can be trusted for read/export workflows. |
| 46 | + /// </summary> |
| 47 | + public bool CanUseCachedFormulaValues => |
| 48 | + FindFeatureCount("Missing formula caches") == 0 && |
| 49 | + FindFeatureCount("Formula dependency issues") == 0; |
| 50 | + |
| 51 | + /// <summary> |
| 52 | + /// True when OfficeIMO's lightweight evaluator can calculate all discovered formulas without known dependency issues. |
| 53 | + /// </summary> |
| 54 | + public bool CanCalculateFormulas => |
| 55 | + FindFeatureCount("Unsupported formulas") == 0 && |
| 56 | + FindFeatureCount("Formula dependency issues") == 0; |
| 57 | + |
| 58 | + /// <summary> |
| 59 | + /// True when template binding can be attempted without preserve-only or unsupported advanced package features. |
| 60 | + /// </summary> |
| 61 | + public bool CanBindTemplate => !HasAdvancedFeatures; |
| 62 | + |
| 63 | + /// <summary> |
| 64 | + /// True when the workbook is suitable for the first-party report-grade Excel-to-PDF export path. |
| 65 | + /// </summary> |
| 66 | + public bool CanExportPdfReport => |
| 67 | + !HasAdvancedFeatures && |
| 68 | + CanUseCachedFormulaValues; |
| 69 | + |
| 70 | + /// <summary> |
| 71 | + /// Returns true when the requested workflow-level capability can be attempted for this workbook. |
| 72 | + /// </summary> |
| 73 | + /// <param name="capability">The OfficeIMO.Excel workflow capability to check.</param> |
| 74 | + public bool Can(ExcelPreflightCapability capability) { |
| 75 | + switch (capability) { |
| 76 | + case ExcelPreflightCapability.ReadWorkbookData: |
| 77 | + return CanReadWorkbookData; |
| 78 | + case ExcelPreflightCapability.EditCellValues: |
| 79 | + return CanEditCellValues; |
| 80 | + case ExcelPreflightCapability.EditWorkbookStructure: |
| 81 | + return CanEditWorkbookStructure; |
| 82 | + case ExcelPreflightCapability.UseCachedFormulaValues: |
| 83 | + return CanUseCachedFormulaValues; |
| 84 | + case ExcelPreflightCapability.CalculateFormulas: |
| 85 | + return CanCalculateFormulas; |
| 86 | + case ExcelPreflightCapability.BindTemplate: |
| 87 | + return CanBindTemplate; |
| 88 | + case ExcelPreflightCapability.ExportPdfReport: |
| 89 | + return CanExportPdfReport; |
| 90 | + default: |
| 91 | + throw new ArgumentOutOfRangeException(nameof(capability), capability, "Unsupported Excel preflight capability."); |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + /// <summary> |
| 96 | + /// Throws with workflow-specific diagnostics when the requested capability cannot be attempted for this workbook. |
| 97 | + /// </summary> |
| 98 | + /// <param name="capability">The OfficeIMO.Excel workflow capability that must be available.</param> |
| 99 | + /// <returns>The current feature report for fluent guard usage.</returns> |
| 100 | + public ExcelFeatureReport EnsureCan(ExcelPreflightCapability capability) { |
| 101 | + if (Can(capability)) { |
| 102 | + return this; |
| 103 | + } |
| 104 | + |
| 105 | + IReadOnlyList<string> diagnostics = GetCapabilityDiagnostics(capability); |
| 106 | + string detail = diagnostics.Count == 0 |
| 107 | + ? "No additional diagnostics were reported." |
| 108 | + : string.Join("; ", diagnostics); |
| 109 | + throw new InvalidOperationException($"Excel preflight capability '{capability}' is not available: {detail}"); |
| 110 | + } |
| 111 | + |
| 112 | + /// <summary> |
| 113 | + /// Returns operation-specific diagnostics explaining why a workflow-level capability is blocked, or an empty list when it can be attempted. |
| 114 | + /// </summary> |
| 115 | + /// <param name="capability">The OfficeIMO.Excel workflow capability to explain.</param> |
| 116 | + public IReadOnlyList<string> GetCapabilityDiagnostics(ExcelPreflightCapability capability) { |
| 117 | + if (Can(capability)) { |
| 118 | + return Array.Empty<string>(); |
| 119 | + } |
| 120 | + |
| 121 | + var messages = new List<string>(); |
| 122 | + switch (capability) { |
| 123 | + case ExcelPreflightCapability.ReadWorkbookData: |
| 124 | + AddUnsupportedDiagnostics(messages, "Workbook data reads are blocked by unsupported workbook features."); |
| 125 | + break; |
| 126 | + case ExcelPreflightCapability.EditCellValues: |
| 127 | + AddUnsupportedDiagnostics(messages, "Cell-value edits are blocked by unsupported workbook features."); |
| 128 | + break; |
| 129 | + case ExcelPreflightCapability.EditWorkbookStructure: |
| 130 | + AddUnsupportedDiagnostics(messages, "Structure-changing edits are blocked by unsupported workbook features."); |
| 131 | + AddPreservedDiagnostics(messages, "Structure-changing edits should not be attempted while preserve-only workbook features are present."); |
| 132 | + break; |
| 133 | + case ExcelPreflightCapability.UseCachedFormulaValues: |
| 134 | + AddFormulaDiagnostics(messages, requireCachedValues: true, requireSupportedFormulas: false); |
| 135 | + break; |
| 136 | + case ExcelPreflightCapability.CalculateFormulas: |
| 137 | + AddFormulaDiagnostics(messages, requireCachedValues: false, requireSupportedFormulas: true); |
| 138 | + break; |
| 139 | + case ExcelPreflightCapability.BindTemplate: |
| 140 | + AddUnsupportedDiagnostics(messages, "Template binding is blocked by unsupported workbook features."); |
| 141 | + AddPreservedDiagnostics(messages, "Template binding should not be attempted while preserve-only workbook features are present unless the workflow has explicit preservation coverage for those parts."); |
| 142 | + break; |
| 143 | + case ExcelPreflightCapability.ExportPdfReport: |
| 144 | + AddUnsupportedDiagnostics(messages, "Excel-to-PDF report export is blocked by unsupported workbook features."); |
| 145 | + AddPreservedDiagnostics(messages, "Excel-to-PDF report export does not render preserve-only workbook features."); |
| 146 | + AddFormulaDiagnostics(messages, requireCachedValues: true, requireSupportedFormulas: false); |
| 147 | + break; |
| 148 | + default: |
| 149 | + throw new ArgumentOutOfRangeException(nameof(capability), capability, "Unsupported Excel preflight capability."); |
| 150 | + } |
| 151 | + |
| 152 | + if (messages.Count == 0) { |
| 153 | + AddDistinct(messages, "The requested Excel workflow is not available for this workbook."); |
| 154 | + } |
| 155 | + |
| 156 | + return messages.AsReadOnly(); |
| 157 | + } |
| 158 | + |
| 159 | + private void AddUnsupportedDiagnostics(List<string> messages, string fallbackMessage) { |
| 160 | + if (UnsupportedFeatures.Count == 0) { |
| 161 | + return; |
| 162 | + } |
| 163 | + |
| 164 | + AddDistinct(messages, fallbackMessage); |
| 165 | + AddFeatureDiagnostics(messages, UnsupportedFeatures); |
| 166 | + } |
| 167 | + |
| 168 | + private void AddPreservedDiagnostics(List<string> messages, string fallbackMessage) { |
| 169 | + if (PreservedFeatures.Count == 0) { |
| 170 | + return; |
| 171 | + } |
| 172 | + |
| 173 | + AddDistinct(messages, fallbackMessage); |
| 174 | + AddFeatureDiagnostics(messages, PreservedFeatures); |
| 175 | + } |
| 176 | + |
| 177 | + private void AddFormulaDiagnostics(List<string> messages, bool requireCachedValues, bool requireSupportedFormulas) { |
| 178 | + if (requireSupportedFormulas) { |
| 179 | + AddFeatureDiagnostics(messages, FindFeatures("Unsupported formulas")); |
| 180 | + } |
| 181 | + |
| 182 | + if (requireCachedValues) { |
| 183 | + AddFeatureDiagnostics(messages, FindFeatures("Missing formula caches")); |
| 184 | + } |
| 185 | + |
| 186 | + AddFeatureDiagnostics(messages, FindFeatures("Formula dependency issues")); |
| 187 | + } |
| 188 | + |
| 189 | + private static void AddFeatureDiagnostics(List<string> messages, IEnumerable<ExcelFeatureFinding> findings) { |
| 190 | + foreach (ExcelFeatureFinding finding in findings) { |
| 191 | + AddDistinct(messages, FormatCapabilityFinding(finding)); |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + private static string FormatCapabilityFinding(ExcelFeatureFinding finding) { |
| 196 | + string message = $"{finding.Name} ({finding.Count}, {finding.SupportLevel}): {finding.Note}"; |
| 197 | + if (finding.Details.Count == 0) { |
| 198 | + return message; |
| 199 | + } |
| 200 | + |
| 201 | + const int maxDetails = 3; |
| 202 | + string details = string.Join("; ", finding.Details.Take(maxDetails)); |
| 203 | + if (finding.Details.Count > maxDetails) { |
| 204 | + details += $"; +{finding.Details.Count - maxDetails} more"; |
| 205 | + } |
| 206 | + |
| 207 | + return message + " [" + details + "]"; |
| 208 | + } |
| 209 | + |
| 210 | + private int FindFeatureCount(string featureName) { |
| 211 | + return FindFeatures(featureName).Sum(feature => feature.Count); |
| 212 | + } |
| 213 | + |
| 214 | + private static void AddDistinct(List<string> messages, string message) { |
| 215 | + if (string.IsNullOrWhiteSpace(message)) { |
| 216 | + return; |
| 217 | + } |
| 218 | + |
| 219 | + for (int i = 0; i < messages.Count; i++) { |
| 220 | + if (string.Equals(messages[i], message, StringComparison.Ordinal)) { |
| 221 | + return; |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + messages.Add(message); |
| 226 | + } |
| 227 | + } |
| 228 | +} |
0 commit comments