berblom 0.1.0

A novel web-of-trust algorithm for trust calculation.
Documentation
use std::collections::HashSet;

use crate::{graph::FlowGraph, types::path::Path};

/// Produce a mermaid "flowchart" representation of `network`, with a heavy focus on being easy to
/// grasp by human readers.
///
/// Also provide the ability to highlight a given `path`, if provided.
///
/// # Note
///
/// **BEWARE:** this is not a robust or general tool!
///
/// This export mechanism is an optimistic hack that works nicely for very simple networks with
/// short/favorable names, but will break in many ways for networks that don't conform to its
/// expectations.
///
/// In addition, even moderately sized networks typically turn out unreadable in mermaid format.
/// This function is mostly intended to visualize example networks, e.g. to show in documentation
/// or to debug test cases.
///
/// The readability of mermaid output benefits greatly from short certificate identifiers.
///
/// Regexes are transformed into a more human-readable and more mermaid-robust form (however, this
/// too is a hack that will break for regex formats that deviate from one particular expected
/// schema!).
///
/// Some special symbols in identifiers will entirely break mermaid rendering!
/// This code currently only does some minimal escaping.
pub(crate) fn mermaid_graph(graph: &FlowGraph, path: Option<&Path>) -> String {
    // In Mermaid, edges are styled by directly referencing them via their ID.
    // The edge ID is simply the index of the edge in the diagram. The first edge is ID 0, the
    // second is ID 1, and so on. Only edges are counted—other elements don’t affect the numbering.
    // Mermaid also doesn't differentiate between different types of edges.
    let mut edge_ids_to_color: Vec<usize> = Vec::new();
    let mut number_of_links = 0;

    // Create a lookup table so that we can easily determine whether a reverse edge is in the path
    // or not.
    let mut reverse_edges_in_path = HashSet::new();
    let mut delegations_in_path = HashSet::new();
    if let Some(path) = &path {
        delegations_in_path = path
            .edges
            .iter()
            .filter_map(|edge| {
                let delegation = edge.delegation()?;
                Some((delegation.issuer().clone(), delegation.target().clone()))
            })
            .collect();

        reverse_edges_in_path = path
            .edges
            .iter()
            .filter_map(|edge| {
                let reverse_edge = edge.reverse()?;
                Some((reverse_edge.issuer.clone(), reverse_edge.target.clone()))
            })
            .collect();
    }

    let mut checked_nodes = HashSet::new();

    let mut out: String = "flowchart TD\n".into();

    // Push the certifications. This usually results in a better layout on mermaid.
    for certification in &graph.target_node().certifications {
        out.push_str(&format!(
            "    {} ==> binding\n",
            &certification.inner.issuer
        ));

        // Mark the certification for coloring if it's in the path.
        if let Some(path) = &path
            && *certification == path.certification
        {
            edge_ids_to_color.push(number_of_links);
        }
        number_of_links += 1;
    }

    out.push_str(&format!(
        "    binding([{}, {}])\n",
        &graph.target_node().binding.cert,
        &graph
            .target_node()
            .binding
            .identity
            .to_string()
            .replace("<", "")
            .replace(">", "")
            .replace("@", "\\@")
            .replace("(", "_")
            .replace(")", "_")
            .replace("\"", "'")
    ));

    out.push('\n');

    // Handle the remaining nodes, delegations and reverse edges
    for node in graph.nodes().values() {
        // Render the node.
        // - trust anchors: diamond shape
        // - nodes: default rectangle shape
        // - bindings are created and styled at the end.
        if !checked_nodes.contains(&node.id) {
            if graph.trust_anchors().contains_key(&node.id) {
                out.push_str(&format!(
                    "    {}@{{ shape: diamond, label: \"{}(t:{})\" }}\n",
                    &node.id,
                    &node.id,
                    node.available_amount()
                ));
            } else {
                out.push_str(&format!(
                    "    {}@{{ shape: rounded, label: \"{}(t:{})\" }}\n",
                    &node.id,
                    &node.id,
                    node.available_amount()
                ));
            }
            checked_nodes.insert(node.id.clone());
        }

        for delegation in &node.delegations {
            let regexes = if !delegation.regexes().is_empty() {
                // Transform regexes ("<[^>]+[@.]example\\.org>$") into nicely human-readable
                // domain names for the graph/
                //
                // FIXME: This code is rather optimistic.
                let mut regexes = delegation
                    .regexes()
                    .iter()
                    .map(|r| {
                        r.to_string()
                            .strip_prefix("<[^>]+[@.]")
                            .unwrap()
                            .strip_suffix(">$")
                            .unwrap()
                            .replace("\\", "")
                    })
                    .collect::<Vec<_>>()
                    .join(",");

                regexes.insert(0, '/');

                regexes
            } else {
                "".to_string()
            };

            out.push_str(&format!(
                "    {} -->|t:{} d:{}{}| {}\n",
                delegation.issuer(),
                delegation.trust_amount(),
                u8::from(delegation.trust_depth()),
                regexes,
                delegation.target(),
            ));

            // Mark the delegation for coloring if it's in the path.
            if delegations_in_path
                .contains(&(delegation.issuer().clone(), delegation.target().clone()))
            {
                edge_ids_to_color.push(number_of_links);
            }
            number_of_links += 1;
        }

        for reverse_edge in &node.reverse_edges {
            out.push_str(&format!(
                "    {} -.->|t:{} d:{}| {}\n",
                &reverse_edge.issuer,
                reverse_edge.trust_amount,
                u8::from(reverse_edge.trust_depth),
                &reverse_edge.target,
            ));

            // Mark the reverse_edge for coloring if it's in the path.
            if reverse_edges_in_path
                .contains(&(reverse_edge.issuer.clone(), reverse_edge.target.clone()))
            {
                edge_ids_to_color.push(number_of_links);
            }
            number_of_links += 1;
        }
    }

    // Color any edges that have been marked.
    out.push('\n');
    for id in edge_ids_to_color {
        out.push_str(&format!(
            "linkStyle {id} stroke:#e74c3c,stroke-width:1px;\n"
        ));
    }

    out
}