Skip to main content

p2panda_auth/group/
display.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use 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    /// Print an auth group graph in DOT format for visualizing the group operation DAG.
34    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                // @TODO: only add edges for nodes which exist in the graph.
117                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}