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