using System.Text.Json; namespace ScadaLink.CLI.Commands; /// /// Human-readable table formatter for audit query --format table (Audit Log /// #23 M8-T6). Renders each fetched page as a column-aligned text table with a fixed /// column set (). Long free-text fields (Target, Actor) are /// truncated with an ellipsis so columns stay aligned regardless of payload size. /// /// /// A header row is emitted once per page (matching the streamable, page-at-a-time /// contract of ). An empty page emits the header only, /// so the column shape is visible even with zero results. /// public sealed class TableAuditFormatter : IAuditFormatter { /// JSON property name (camelCase, as the server serializes it) → column header. private static readonly (string Property, string Header, int MaxWidth)[] Columns = { ("occurredAtUtc", "OccurredAtUtc", 24), ("channel", "Channel", 14), ("kind", "Kind", 18), ("status", "Status", 12), ("target", "Target", 32), ("actor", "Actor", 20), ("durationMs", "DurationMs", 10), ("httpStatus", "HttpStatus", 10), }; public void WritePage(IReadOnlyList events, TextWriter output) { // Build every cell first so column widths account for the actual data. var rows = new List(events.Count); foreach (var evt in events) { var cells = new string[Columns.Length]; for (var i = 0; i < Columns.Length; i++) cells[i] = Truncate(CellValue(evt, Columns[i].Property), Columns[i].MaxWidth); rows.Add(cells); } var widths = new int[Columns.Length]; for (var i = 0; i < Columns.Length; i++) widths[i] = Columns[i].Header.Length; foreach (var row in rows) for (var i = 0; i < Columns.Length; i++) widths[i] = Math.Max(widths[i], row[i].Length); WriteRow(output, Columns.Select(c => c.Header).ToArray(), widths); foreach (var row in rows) WriteRow(output, row, widths); } /// /// Extracts a cell value for from an audit event. /// A missing property or a JSON null renders as an empty string (never /// the literal text "null"). /// private static string CellValue(JsonElement evt, string property) { if (evt.ValueKind != JsonValueKind.Object || !evt.TryGetProperty(property, out var value) || value.ValueKind == JsonValueKind.Null) { return string.Empty; } return value.ValueKind == JsonValueKind.String ? value.GetString() ?? string.Empty : value.ToString(); } /// /// Truncates to characters, /// replacing the tail with a single-character ellipsis so the column stays aligned. /// private static string Truncate(string value, int maxWidth) { if (maxWidth <= 0 || value.Length <= maxWidth) return value; if (maxWidth == 1) return "…"; return value.Substring(0, maxWidth - 1) + "…"; } private static void WriteRow(TextWriter output, IReadOnlyList cells, int[] widths) { for (var i = 0; i < cells.Count; i++) { // Last column is not padded — avoids trailing whitespace at line end. output.Write(i == cells.Count - 1 ? cells[i] : cells[i].PadRight(widths[i] + 2)); } output.WriteLine(); } }