1use std::collections::HashSet;
4use std::fmt::{Debug, Display};
5
6use petgraph::algo::toposort;
7use petgraph::dot::{Config, Dot};
8use petgraph::graph::{DiGraph, NodeIndex};
9use petgraph::visit::IntoNodeReferences;
10
11use crate::group::crdt::StateChangeResult;
12use crate::group::{GroupAction, GroupControlMessage, GroupCrdtState, GroupMember, apply_action};
13use crate::traits::{Conditions, IdentityHandle, Operation, OperationId, Orderer};
14
15const OP_FILTER_NODE: &str = "#E63C3F";
16const OP_MUTUAL_REMOVE_NODE: &str = "#9a0aad";
17const OP_OK_NODE: &str = "#BFC6C77F";
18const OP_ERR_NODE: &str = "#FFA142";
19const OP_ROOT_NODE: &str = "#EDD7B17F";
20const INDIVIDUAL_NODE: &str = "#EDD7B17F";
21const ADD_MEMBER_EDGE: &str = "#0091187F";
22const DEPENDENCIES_EDGE: &str = "#B748E37F";
23
24impl<ID, OP, C, ORD> GroupCrdtState<ID, OP, C, ORD>
25where
26 ID: IdentityHandle + Ord + Display,
27 OP: OperationId + Ord + Display,
28 C: Conditions,
29 ORD: Orderer<ID, OP, GroupControlMessage<ID, C>> + Clone + Debug,
30 ORD::State: Clone,
31 ORD::Operation: Clone,
32{
33 pub fn display(&self, group_id: ID) -> String {
35 let mut graph = DiGraph::new();
36 graph = self.add_nodes_and_previous_edges(graph);
37
38 graph.add_node((None, self.format_final_members(group_id)));
39
40 let dag_graphviz = Dot::with_attr_getters(
41 &graph,
42 &[Config::NodeNoLabel, Config::EdgeNoLabel],
43 &|_, edge| {
44 let weight = edge.weight();
45 if weight == "member" || weight == "sub group" {
46 return format!("color=\"{ADD_MEMBER_EDGE}\"");
47 }
48
49 format!("color=\"{DEPENDENCIES_EDGE}\"")
50 },
51 &|_, (_, (_, s))| format!("label = {s}"),
52 );
53
54 let mut s = format!("{dag_graphviz:?}");
55 s = s.replace("digraph {", "digraph {\n splines=polyline\n");
56 s
57 }
58
59 fn add_nodes_and_previous_edges(
60 &self,
61 mut graph: DiGraph<(Option<OP>, String), String>,
62 ) -> DiGraph<(Option<OP>, String), String> {
63 let sorted = toposort(&self.inner.graph, None).expect("topo sort graph");
64 for id in sorted {
65 let operation = self
66 .inner
67 .operations
68 .get(&id)
69 .expect("operation is present");
70 graph.add_node((Some(operation.id()), self.format_operation(operation)));
71
72 let (operation_idx, _) = graph
73 .node_references()
74 .find(|(_, (op, _))| {
75 if let Some(op) = op {
76 *op == operation.id()
77 } else {
78 false
79 }
80 })
81 .unwrap();
82
83 if let GroupControlMessage {
84 action: GroupAction::Add { member, .. },
85 ..
86 } = operation.payload()
87 {
88 graph = self.add_member_to_graph(operation_idx, member, graph);
89 }
90
91 if let GroupControlMessage {
92 action:
93 GroupAction::Create {
94 initial_members, ..
95 },
96 ..
97 } = operation.payload()
98 {
99 for (member, _access) in initial_members {
100 graph = self.add_member_to_graph(operation_idx, member, graph);
101 }
102 }
103
104 for dependency in operation.dependencies() {
105 let (idx, _) = graph
106 .node_references()
107 .find(|(_, (op, _))| {
108 if let Some(op) = op {
109 *op == dependency
110 } else {
111 false
112 }
113 })
114 .unwrap();
115
116 graph.add_edge(operation_idx, idx, "dependency".to_string());
118 }
119 }
120
121 graph
122 }
123
124 fn format_operation(&self, operation: &ORD::Operation) -> String {
125 let control_message = operation.payload();
126
127 let mut s = String::new();
128
129 let color = if control_message.is_create() {
130 OP_ROOT_NODE
131 } else {
132 let groups_y = self
133 .inner
134 .state_at(&HashSet::from_iter(operation.dependencies()))
135 .unwrap();
136
137 if self.inner.mutual_removes.contains(&operation.id()) {
138 OP_MUTUAL_REMOVE_NODE
139 } else {
140 match apply_action(
141 groups_y,
142 control_message.group_id(),
143 operation.id(),
144 operation.author(),
145 &control_message.action,
146 &self.inner.ignore,
147 ) {
148 StateChangeResult::Ok { .. } => OP_OK_NODE,
149 StateChangeResult::Error { .. } => OP_ERR_NODE,
150 StateChangeResult::Filtered { .. } => OP_FILTER_NODE,
151 }
152 }
153 };
154
155 s += &format!(
156 "<<TABLE BGCOLOR=\"{color}\" BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">"
157 );
158 s += &format!(
159 "<TR><TD>group</TD><TD>{}</TD></TR>",
160 control_message.group_id()
161 );
162 s += &format!("<TR><TD>operation id</TD><TD>{}</TD></TR>", operation.id());
163 s += &format!("<TR><TD>actor</TD><TD>{}</TD></TR>", operation.author());
164 let dependencies = operation.dependencies().clone();
165 if !dependencies.is_empty() {
166 s += &format!(
167 "<TR><TD>dependencies</TD><TD>{}</TD></TR>",
168 self.format_dependencies(&dependencies)
169 );
170 }
171 s += &format!(
172 "<TR><TD COLSPAN=\"2\">{}</TD></TR>",
173 self.format_control_message(&control_message)
174 );
175 s += &format!(
176 "<TR><TD COLSPAN=\"2\">{}</TD></TR>",
177 self.format_members(operation)
178 );
179 s += "</TABLE>>";
180 s
181 }
182
183 fn format_final_members(&self, group_id: ID) -> String {
184 let mut s = String::new();
185 s += "<<TABLE BGCOLOR=\"#00E30F7F\" BORDER=\"1\" CELLBORDER=\"1\" CELLSPACING=\"2\">";
186
187 let members = self.members(group_id);
188 s += "<TR><TD>GROUP MEMBERS</TD></TR>";
189 for (id, access) in members {
190 s += &format!("<TR><TD> {id} : {access} </TD></TR>");
191 }
192 s += "</TABLE>>";
193 s
194 }
195
196 fn format_control_message(&self, message: &GroupControlMessage<ID, C>) -> String {
197 let mut s = String::new();
198 s += "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">";
199
200 match &message.action {
201 GroupAction::Create { initial_members } => {
202 s += "<TR><TD>CREATE</TD></TR>";
203 s += "<TR><TD>initial members</TD></TR>";
204 for (member, access) in initial_members {
205 match member {
206 GroupMember::Individual(id) => {
207 s += &format!("<TR><TD>individual : {id} : {access}</TD></TR>")
208 }
209 GroupMember::Group(id) => {
210 s += &format!("<TR><TD>group : {id} : {access}</TD></TR>")
211 }
212 }
213 }
214 }
215 GroupAction::Add { member, access } => {
216 s += "<TR><TD>ADD</TD></TR>";
217 match member {
218 GroupMember::Individual(id) => {
219 s += &format!("<TR><TD>individual : {id} : {access}</TD></TR>")
220 }
221 GroupMember::Group(id) => {
222 s += &format!("<TR><TD>group : {id} : {access}</TD></TR>")
223 }
224 }
225 }
226 GroupAction::Remove { member } => {
227 s += "<TR><TD>REMOVE</TD></TR>";
228 match member {
229 GroupMember::Individual(id) => {
230 s += &format!("<TR><TD>individual : {id}</TD></TR>")
231 }
232 GroupMember::Group(id) => s += &format!("<TR><TD>group : {id}</TD></TR>"),
233 }
234 }
235 GroupAction::Promote { member, access } => {
236 s += "<TR><TD>PROMOTE</TD></TR>";
237 match member {
238 GroupMember::Individual(id) => {
239 s += &format!("<TR><TD>individual : {id} : {access}</TD></TR>")
240 }
241 GroupMember::Group(id) => {
242 s += &format!("<TR><TD>group : {id} : {access}</TD></TR>")
243 }
244 }
245 }
246 GroupAction::Demote { member, access } => {
247 s += "<TR><TD>DEMOTE</TD></TR>";
248 match member {
249 GroupMember::Individual(id) => {
250 s += &format!("<TR><TD>individual : {id} : {access}</TD></TR>")
251 }
252 GroupMember::Group(id) => {
253 s += &format!("<TR><TD>group : {id} : {access}</TD></TR>")
254 }
255 }
256 }
257 }
258 s += "</TABLE>";
259 s
260 }
261
262 fn format_members(&self, operation: &ORD::Operation) -> String {
263 let mut dependencies = HashSet::from_iter(operation.dependencies().clone());
264 dependencies.insert(operation.id());
265 let mut members = self
266 .inner
267 .state_at(&dependencies)
268 .unwrap()
269 .get(&operation.payload().group_id())
270 .unwrap()
271 .access_levels();
272 members.sort_by(|(id_a, _), (id_b, _)| id_a.cmp(id_b));
273
274 let mut s = String::new();
275 s += "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">";
276 s += "<TR><TD>MEMBERS</TD></TR>";
277
278 for (member, access) in members {
279 s += &format!("<TR><TD>{member:?} : {access}</TD></TR>")
280 }
281
282 s += "</TABLE>";
283 s
284 }
285
286 fn format_dependencies(&self, dependencies: &Vec<OP>) -> String {
287 let mut s = String::new();
288 s += "<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">";
289
290 for id in dependencies {
291 s += &format!("<TR><TD>{id}</TD></TR>")
292 }
293
294 s += "</TABLE>";
295 s
296 }
297
298 fn add_member_to_graph(
299 &self,
300 operation_idx: NodeIndex,
301 member: GroupMember<ID>,
302 mut graph: DiGraph<(Option<OP>, String), String>,
303 ) -> DiGraph<(Option<OP>, String), String> {
304 match member {
305 GroupMember::Individual(id) => {
306 let table = format!(
307 "<<TABLE BGCOLOR=\"{INDIVIDUAL_NODE}\" BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\"><TR><TD>individual</TD><TD>{id}</TD></TR></TABLE>>"
308 );
309 let idx = match graph.node_references().find(|(_, (_, t))| t == &table) {
310 Some((idx, _)) => idx,
311 None => graph.add_node((None, table)),
312 };
313 graph.add_edge(operation_idx, idx, "member".to_string());
314 }
315 GroupMember::Group(group_id) => {
316 let (create_group_id, _) = self
317 .inner
318 .operations
319 .iter()
320 .find(|(_, op)| op.payload().is_create() && op.payload().group_id() == group_id)
321 .unwrap();
322 let (idx, _) = graph
323 .node_references()
324 .find(|(_, (op, _))| *op == Some(*create_group_id))
325 .unwrap();
326 graph.add_edge(operation_idx, idx, "member".to_string());
327 }
328 }
329 graph
330 }
331}