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::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    /// Print an auth group graph in DOT format for visualizing the group operation DAG.
32    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                // @TODO: only add edges for nodes which exist in the graph.
107                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}