wot-network 0.0.6

Data structures for OpenPGP Web of Trust calculations
Documentation
//! Render a [Network] as mermaid flowchart

use crate::{Binding, Network};

/// Produce a mermaid "flowchart" representation of `network`, with a heavy focus on being easy to
/// grasp by human readers.
///
/// 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.
///
/// The readability of mermaid output benefits greatly from short certificate identifiers
/// ([crate::util::mapping::map_names] can be used to map wot network data to a representation with
/// short 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 fn graph(network: &Network) -> String {
    let mut out: String = "flowchart TD\n".into();

    for delegation in network.delegations.values().flatten() {
        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.0)
                .map(|s| {
                    s.strip_prefix("<[^>]+[@.]")
                        .unwrap()
                        .strip_suffix(">$")
                        .unwrap()
                })
                .map(|s| s.replace("\\", ""))
                .collect::<Vec<_>>()
                .join(",");

            regexes.insert(0, '/');

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

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

    out.push('\n');

    for (i, (Binding { cert, identity }, certifications)) in
        network.certifications.iter().enumerate()
    {
        // shorthand name for this binding in mermaid
        let shorthand = format!("b{}", i);

        for certification in certifications {
            out.push_str(&format!(
                "    {} -..-> {}\n",
                &certification.issuer.0, shorthand
            ))
        }

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

        out.push('\n');
    }

    out.push_str("classDef bind stroke-dasharray: 5 5\n");
    let binds: String = (0..network.certifications.len())
        .map(|i| format!("b{}", i))
        .collect::<Vec<_>>()
        .join(",");
    out.push_str(&format!("class {} bind;", binds));

    out
}