using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace JobTrackerApi.Models; public sealed record DashboardWidget(string Id, bool Visible, string Size, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] int? X = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] int? Y = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] int? W = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] int? H = null); public sealed record DashboardLayout(int Version, long Revision, IReadOnlyList Widgets) { public const int MaxBytes = 16 * 1024; // Responses cross a JavaScript client; the incremented revision must remain an exact integer there. private const long MaxRevision = 9_007_199_254_740_991L; private static readonly HashSet KnownIds = new(StringComparer.Ordinal) { "cards", "attention", "funnel", "activity", "stageAge", "sources", "salary", "companies", "skills" }; public static DashboardLayout? Parse(JsonElement root, bool reading = false, long revision = 0) { if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty("version", out var version) || version.ValueKind != JsonValueKind.Number || !version.TryGetInt32(out var v) || v is not (1 or 2) || !root.TryGetProperty("widgets", out var widgets) || widgets.ValueKind != JsonValueKind.Array || widgets.GetArrayLength() > 32) return null; if (!reading && (!root.TryGetProperty("revision", out var rev) || rev.ValueKind != JsonValueKind.Number || !rev.TryGetInt64(out revision) || revision < 0 || revision >= MaxRevision)) return null; var result = new List(); var seen = new HashSet(StringComparer.Ordinal); foreach (var widget in widgets.EnumerateArray()) { if (widget.ValueKind != JsonValueKind.Object || !widget.TryGetProperty("id", out var idValue) || idValue.ValueKind != JsonValueKind.String || !KnownIds.Contains(idValue.GetString()!) || !seen.Add(idValue.GetString()!) || !widget.TryGetProperty("visible", out var visible) || (visible.ValueKind != JsonValueKind.True && visible.ValueKind != JsonValueKind.False)) { if (reading) continue; return null; } var id = idValue.GetString()!; var size = widget.TryGetProperty("size", out var sizeValue) && sizeValue.ValueKind == JsonValueKind.String ? sizeValue.GetString() : null; if (size is not ("standard" or "full") || (id == "cards" && size != "full")) { if (!reading) return null; size = id == "cards" ? "full" : "standard"; } if (v == 1) result.Add(new DashboardWidget(id, visible.GetBoolean(), size)); else { static bool Integer(JsonElement item, string key, out int number) { number = 0; return item.TryGetProperty(key, out var field) && field.ValueKind == JsonValueKind.Number && field.TryGetInt32(out number); } if (!Integer(widget, "x", out var x) || !Integer(widget, "y", out var y) || !Integer(widget, "w", out var w) || !Integer(widget, "h", out var h) || x is < 0 or > 12 || y is < 0 or > 256 || w is < 4 or > 12 || h is < 4 or > 24 || x + w > 12 || y + h > 256 || (id == "cards" && w != 12) || (!reading && size != (w == 12 ? "full" : "standard"))) { if (reading) continue; return null; } result.Add(new DashboardWidget(id, visible.GetBoolean(), w == 12 ? "full" : "standard", x, y, w, h)); } } if (!reading && v == 2) { var visibleWidgets = result.Where(widget => widget.Visible).ToArray(); for (var first = 0; first < visibleWidgets.Length; first++) for (var second = first + 1; second < visibleWidgets.Length; second++) { var a = visibleWidgets[first]; var b = visibleWidgets[second]; if (a.X < b.X + b.W && a.X + a.W > b.X && a.Y < b.Y + b.H && a.Y + a.H > b.Y) return null; } } return new DashboardLayout(v, revision, result); } public static DashboardLayout Read(string? json, long revision) { if (json is not null && Encoding.UTF8.GetByteCount(json) <= MaxBytes) { try { using var document = JsonDocument.Parse(json); if (document.RootElement.ValueKind == JsonValueKind.Object && document.RootElement.TryGetProperty("version", out var version) && version.ValueKind == JsonValueKind.Number && version.TryGetInt32(out var futureVersion) && futureVersion > 2) return new(futureVersion, revision, []); return Parse(document.RootElement, reading: true, revision) ?? new(1, revision, []); } catch (JsonException) { } } return new(1, revision, []); } }