| 1 | #nullable enable |
| 2 | using System.Text.Json; |
| 3 | |
| 4 | namespace CascadeIDE.Cockpit.Graph; |
| 5 | |
| 6 | /// <summary>Парсинг wire JSON режима <c>subgraph</c> / <c>related</c> в <see cref="GraphDocument"/> (ADR 0067).</summary> |
| 7 | public static class GraphDocumentJson |
| 8 | { |
| 9 | public static bool TryParse(string json, out GraphDocument? doc, out string? error) |
| 10 | { |
| 11 | doc = null; |
| 12 | error = null; |
| 13 | try |
| 14 | { |
| 15 | using var j = JsonDocument.Parse(json); |
| 16 | return TryParseRoot(j.RootElement, out doc, out error); |
| 17 | } |
| 18 | catch (Exception ex) |
| 19 | { |
| 20 | error = ex.Message; |
| 21 | return false; |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | /// <summary>Subgraph или related (для уровня file при graph/both).</summary> |
| 26 | public static bool TryParseRoot(JsonElement root, out GraphDocument? doc, out string? error) |
| 27 | { |
| 28 | doc = null; |
| 29 | error = null; |
| 30 | if (root.TryGetProperty("error", out var errEl)) |
| 31 | { |
| 32 | error = errEl.GetString() ?? "error"; |
| 33 | return false; |
| 34 | } |
| 35 | |
| 36 | if (!root.TryGetProperty("mode", out var modeEl) || modeEl.ValueKind != JsonValueKind.String) |
| 37 | { |
| 38 | error = "bad_mode"; |
| 39 | return false; |
| 40 | } |
| 41 | |
| 42 | return modeEl.GetString() switch |
| 43 | { |
| 44 | "subgraph" => TryParseSubgraphRoot(root, out doc, out error), |
| 45 | "related" => TryParseRelatedRoot(root, out doc, out error), |
| 46 | _ => Fail(out doc, out error, "bad_mode") |
| 47 | }; |
| 48 | } |
| 49 | |
| 50 | private static bool TryParseSubgraphRoot(JsonElement root, out GraphDocument? doc, out string? error) |
| 51 | { |
| 52 | doc = null; |
| 53 | error = null; |
| 54 | |
| 55 | var anchor = root.TryGetProperty("anchor_path", out var ap) && ap.ValueKind == JsonValueKind.String |
| 56 | ? ap.GetString() ?? "" |
| 57 | : ""; |
| 58 | if (string.IsNullOrEmpty(anchor)) |
| 59 | return Fail(out doc, out error, "no_anchor"); |
| 60 | |
| 61 | var graphKind = TryParseGraphKind(root); |
| 62 | var nodes = ParseSubgraphNodes(root); |
| 63 | var edges = ParseSubgraphEdges(root); |
| 64 | |
| 65 | doc = new GraphDocument |
| 66 | { |
| 67 | AnchorPath = anchor, |
| 68 | Kind = graphKind, |
| 69 | Nodes = nodes, |
| 70 | Edges = edges |
| 71 | }; |
| 72 | return true; |
| 73 | } |
| 74 | |
| 75 | private static bool TryParseRelatedRoot(JsonElement root, out GraphDocument? doc, out string? error) |
| 76 | { |
| 77 | doc = null; |
| 78 | error = null; |
| 79 | |
| 80 | var anchor = root.TryGetProperty("anchor_path", out var ap) && ap.ValueKind == JsonValueKind.String |
| 81 | ? ap.GetString() ?? "" |
| 82 | : ""; |
| 83 | if (string.IsNullOrEmpty(anchor)) |
| 84 | return Fail(out doc, out error, "no_anchor"); |
| 85 | |
| 86 | if (!root.TryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array) |
| 87 | return Fail(out doc, out error, "no_items"); |
| 88 | |
| 89 | var nodes = new List<GraphNode> |
| 90 | { |
| 91 | new() |
| 92 | { |
| 93 | Id = "n0", |
| 94 | Path = anchor, |
| 95 | Kind = "anchor", |
| 96 | Label = Path.GetFileName(anchor) |
| 97 | } |
| 98 | }; |
| 99 | var edges = new List<GraphEdge>(); |
| 100 | var idByPath = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { [anchor] = "n0" }; |
| 101 | var n = 1; |
| 102 | |
| 103 | foreach (var el in items.EnumerateArray()) |
| 104 | { |
| 105 | var path = el.TryGetProperty("path", out var pEl) ? pEl.GetString() : null; |
| 106 | if (string.IsNullOrEmpty(path)) |
| 107 | continue; |
| 108 | |
| 109 | if (idByPath.ContainsKey(path)) |
| 110 | continue; |
| 111 | |
| 112 | var id = $"n{n++}"; |
| 113 | idByPath[path] = id; |
| 114 | var relatedKind = el.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() : null; |
| 115 | var semanticKind = string.IsNullOrEmpty(relatedKind) ? "related" : relatedKind; |
| 116 | var rel = el.TryGetProperty("relative_path", out var rEl) ? rEl.GetString() : null; |
| 117 | var rat = el.TryGetProperty("rationale", out var raEl) ? raEl.GetString() : null; |
| 118 | nodes.Add(new GraphNode |
| 119 | { |
| 120 | Id = id, |
| 121 | Path = path, |
| 122 | Kind = semanticKind, |
| 123 | Label = Path.GetFileName(path), |
| 124 | RelativePath = rel, |
| 125 | Rationale = rat |
| 126 | }); |
| 127 | edges.Add(new GraphEdge |
| 128 | { |
| 129 | FromId = "n0", |
| 130 | ToId = id, |
| 131 | Kind = "related_to", |
| 132 | RelationKind = relatedKind |
| 133 | }); |
| 134 | } |
| 135 | |
| 136 | doc = new GraphDocument |
| 137 | { |
| 138 | AnchorPath = anchor, |
| 139 | Kind = GraphKind.RelatedFiles, |
| 140 | Nodes = nodes, |
| 141 | Edges = edges |
| 142 | }; |
| 143 | return true; |
| 144 | } |
| 145 | |
| 146 | private static List<GraphNode> ParseSubgraphNodes(JsonElement root) |
| 147 | { |
| 148 | var nodes = new List<GraphNode>(); |
| 149 | if (!root.TryGetProperty("nodes", out var nodesEl) || nodesEl.ValueKind != JsonValueKind.Array) |
| 150 | return nodes; |
| 151 | |
| 152 | foreach (var el in nodesEl.EnumerateArray()) |
| 153 | { |
| 154 | var id = el.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? "" : ""; |
| 155 | var path = el.TryGetProperty("path", out var pEl) ? pEl.GetString() ?? "" : ""; |
| 156 | if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(path)) |
| 157 | continue; |
| 158 | var kind = el.TryGetProperty("kind", out var kEl) ? kEl.GetString() ?? "" : ""; |
| 159 | var label = el.TryGetProperty("label", out var lEl) ? lEl.GetString() ?? Path.GetFileName(path) : Path.GetFileName(path); |
| 160 | var rel = el.TryGetProperty("relative_path", out var rEl) ? rEl.GetString() : null; |
| 161 | var rat = el.TryGetProperty("rationale", out var raEl) ? raEl.GetString() : null; |
| 162 | int? legendIndex = null; |
| 163 | if (el.TryGetProperty("legend_index", out var liEl) && liEl.ValueKind == JsonValueKind.Number |
| 164 | && liEl.TryGetInt32(out var li)) |
| 165 | legendIndex = li; |
| 166 | var legendText = el.TryGetProperty("legend_text", out var ltEl) ? ltEl.GetString() : null; |
| 167 | int? lineStart = null; |
| 168 | int? lineEnd = null; |
| 169 | if (el.TryGetProperty("line_start", out var lsEl) && lsEl.ValueKind == JsonValueKind.Number |
| 170 | && lsEl.TryGetInt32(out var ls)) |
| 171 | lineStart = ls; |
| 172 | if (el.TryGetProperty("line_end", out var leEl) && leEl.ValueKind == JsonValueKind.Number |
| 173 | && leEl.TryGetInt32(out var le)) |
| 174 | lineEnd = le; |
| 175 | int? loopGroupId = null; |
| 176 | if (el.TryGetProperty("loop_group", out var lgEl) && lgEl.ValueKind == JsonValueKind.Number |
| 177 | && lgEl.TryGetInt32(out var lg)) |
| 178 | loopGroupId = lg; |
| 179 | nodes.Add(new GraphNode |
| 180 | { |
| 181 | Id = id, |
| 182 | Path = path, |
| 183 | Kind = kind, |
| 184 | Label = string.IsNullOrEmpty(label) ? Path.GetFileName(path) : label, |
| 185 | RelativePath = rel, |
| 186 | Rationale = rat, |
| 187 | LegendIndex = legendIndex, |
| 188 | LegendText = legendText, |
| 189 | LineStart = lineStart, |
| 190 | LineEnd = lineEnd, |
| 191 | LoopGroupId = loopGroupId |
| 192 | }); |
| 193 | } |
| 194 | |
| 195 | return nodes; |
| 196 | } |
| 197 | |
| 198 | private static List<GraphEdge> ParseSubgraphEdges(JsonElement root) |
| 199 | { |
| 200 | var edges = new List<GraphEdge>(); |
| 201 | if (!root.TryGetProperty("edges", out var edgesEl) || edgesEl.ValueKind != JsonValueKind.Array) |
| 202 | return edges; |
| 203 | |
| 204 | foreach (var el in edgesEl.EnumerateArray()) |
| 205 | { |
| 206 | var from = el.TryGetProperty("from_id", out var fEl) ? fEl.GetString() ?? "" : ""; |
| 207 | var to = el.TryGetProperty("to_id", out var tEl) ? tEl.GetString() ?? "" : ""; |
| 208 | if (string.IsNullOrEmpty(from) || string.IsNullOrEmpty(to)) |
| 209 | continue; |
| 210 | var k = el.TryGetProperty("kind", out var ke) ? ke.GetString() : null; |
| 211 | var relationKind = el.TryGetProperty("relation_kind", out var rkeNew) && rkeNew.ValueKind == JsonValueKind.String |
| 212 | ? rkeNew.GetString() |
| 213 | : el.TryGetProperty("related_kind", out var rke) ? rke.GetString() : null; |
| 214 | var provenance = el.TryGetProperty("edge_provenance", out var pe) && pe.ValueKind == JsonValueKind.String |
| 215 | ? pe.GetString() |
| 216 | : null; |
| 217 | edges.Add(new GraphEdge |
| 218 | { |
| 219 | FromId = from, |
| 220 | ToId = to, |
| 221 | Kind = k, |
| 222 | RelationKind = relationKind, |
| 223 | EdgeProvenance = provenance |
| 224 | }); |
| 225 | } |
| 226 | |
| 227 | return edges; |
| 228 | } |
| 229 | |
| 230 | private static bool Fail(out GraphDocument? doc, out string? error, string code) |
| 231 | { |
| 232 | doc = null; |
| 233 | error = code; |
| 234 | return false; |
| 235 | } |
| 236 | |
| 237 | public static GraphKind TryParseGraphKind(JsonElement root) |
| 238 | { |
| 239 | if (!root.TryGetProperty("graph_kind", out var g) || g.ValueKind != JsonValueKind.String) |
| 240 | return GraphKind.Unspecified; |
| 241 | var s = g.GetString(); |
| 242 | if (string.IsNullOrEmpty(s)) |
| 243 | return GraphKind.Unspecified; |
| 244 | if (string.Equals(s, GraphKindWire.CodeIntent, StringComparison.Ordinal) |
| 245 | || string.Equals(s, GraphKindWire.CodeIntentLegacy, StringComparison.Ordinal)) |
| 246 | return GraphKind.CodeIntent; |
| 247 | if (string.Equals(s, GraphKindWire.RelatedFiles, StringComparison.Ordinal)) |
| 248 | return GraphKind.RelatedFiles; |
| 249 | if (string.Equals(s, GraphKindWire.RepositoryModuleTree, StringComparison.Ordinal)) |
| 250 | return GraphKind.RepositoryModuleTree; |
| 251 | return GraphKind.Unspecified; |
| 252 | } |
| 253 | } |
| 254 | |