pipecircuit-visualization 0.0.10

Code-derived architectural visualizations for Pipecircuit programs
Documentation
use crate::DiagramTarget;
use pipecircuit::{BindingDescription, DeclarationLocation, PipeDescription};

pub(super) fn render(
    board: &str,
    pipes: &[PipeDescription],
    bindings: Option<&[BindingDescription]>,
    target: DiagramTarget,
) -> String {
    match target {
        DiagramTarget::TextTree | DiagramTarget::AnsiTextTree => text(board, pipes, bindings),
        DiagramTarget::MermaidFlowchart => graph(board, pipes, bindings, true),
        DiagramTarget::GraphvizDot => graph(board, pipes, bindings, false),
    }
}
pub(super) fn source(location: DeclarationLocation) -> String {
    format!(
        "{}:{}:{} ({})",
        location.file, location.line, location.column, location.package
    )
}
fn text(board: &str, pipes: &[PipeDescription], bindings: Option<&[BindingDescription]>) -> String {
    let mut text = format!(
        "Experimental inspection prototype: development review only; not ready for application consumption.\n\nProgram: {}\nSwitchboard: routes messages between the named pipes below.\n\nRead directions from each pipe's perspective:\n  emits (Out) -> switchboard -> receives (In)\nThese are message families, not a request/reply ordering guarantee.\n",
        short_type(board)
    );
    for pipe in pipes {
        text.push_str(&format!(
            "\n{} [{}]\n",
            pipe.name,
            short_type(pipe.protocol)
        ));
        for line in pipe.documentation {
            text.push_str(&format!("  {}\n", line.trim()));
        }
        text.push_str(&format!(
            "  emits to switchboard: {}\n  receives from switchboard: {}\n  declared at: {}\n",
            short_type(pipe.output),
            short_type(pipe.input),
            source(pipe.declaration)
        ));
        if let Some(binding) = bindings.and_then(|all| all.iter().find(|b| b.name == pipe.name)) {
            text.push_str(&format!(
                "  installed with: {}\n",
                short_type(binding.driver)
            ));
            for line in binding.documentation {
                text.push_str(&format!("    {}\n", line.trim()));
            }
            text.push_str(&format!("  wired at: {}\n", source(binding.declaration)));
        }
    }
    text.push_str(
        "\nScope: declared boundaries; runtime routes and reachability are not inferred.\n",
    );
    if bindings.is_some() {
        text.push_str("Transport occupancy, health, remote placement and compatibility are not measured here.\n");
    }
    text
}
fn label(value: &str, mermaid: bool) -> String {
    if mermaid {
        value
            .replace('&', "&amp;")
            .replace('<', "&lt;")
            .replace('>', "&gt;")
            .replace('"', "&quot;")
            .replace('\n', "<br/>")
    } else {
        value
            .replace('\\', "\\\\")
            .replace('"', "\\\"")
            .replace('\n', "\\n")
    }
}
fn node(output: &mut String, id: &str, value: &str, mermaid: bool) {
    let value = label(value, mermaid);
    if mermaid {
        output.push_str(&format!("  {id}[\"{value}\"]\n"));
    } else {
        output.push_str(&format!("  {id} [label=\"{value}\"];\n"));
    }
}
fn edge(output: &mut String, from: &str, to: &str, value: &str, mermaid: bool) {
    if mermaid {
        output.push_str(&format!("  {from} -. {value} .-> {to}\n"));
    } else {
        output.push_str(&format!(
            "  {from} -> {to} [style=dashed,label=\"{value}\"];\n"
        ));
    }
}
fn graph(
    board: &str,
    pipes: &[PipeDescription],
    bindings: Option<&[BindingDescription]>,
    mermaid: bool,
) -> String {
    let mut output = if mermaid {
        String::from(
            "%% Experimental inspection prototype; not ready for application consumption.\nflowchart LR\n",
        )
    } else {
        String::from(
            "// Experimental inspection prototype; not ready for application consumption.\ndigraph program {\n",
        )
    };
    node(&mut output, "board", &short_type(board), mermaid);
    node(
        &mut output,
        "scope",
        "Declared boundaries only; routes and reachability are runtime decisions",
        mermaid,
    );
    for (index, pipe) in pipes.iter().enumerate() {
        let id = format!("pipe{index}");
        node(
            &mut output,
            &id,
            &format!(
                "{}: {}\n{}\nReceives: {}\nEmits: {}\n{}",
                pipe.name,
                short_type(pipe.protocol),
                pipe.documentation
                    .iter()
                    .map(|line| line.trim())
                    .collect::<Vec<_>>()
                    .join("\n"),
                short_type(pipe.input),
                short_type(pipe.output),
                source(pipe.declaration)
            ),
            mermaid,
        );
        edge(&mut output, &id, "board", "emits Out", mermaid);
        edge(&mut output, "board", &id, "receives In", mermaid);
    }
    if let Some(bindings) = bindings {
        node(
            &mut output,
            "physical_scope",
            "Types only; occupancy, health, remote placement and compatibility not inspected",
            mermaid,
        );
        for (index, binding) in bindings.iter().enumerate() {
            let id = format!("binding{index}");
            node(
                &mut output,
                &id,
                &format!(
                    "{}\n{}\n{}",
                    binding.name,
                    short_type(binding.binding),
                    source(binding.declaration)
                ),
                mermaid,
            );
            if let Some(index) = pipes.iter().position(|pipe| pipe.name == binding.name) {
                edge(
                    &mut output,
                    &format!("pipe{index}"),
                    &id,
                    "installed as",
                    mermaid,
                );
            }
        }
    }
    if !mermaid {
        output.push_str("}\n");
    }
    output
}

// Qualified names stay in the inspection data; the diagram abbreviates only paths.
pub(super) fn short_type(value: &str) -> String {
    let mut rendered = String::new();
    let mut path = String::new();
    for character in value.chars() {
        if character.is_alphanumeric() || character == '_' || character == ':' {
            path.push(character);
        } else {
            rendered.push_str(path.rsplit("::").next().unwrap_or(""));
            path.clear();
            rendered.push(character);
        }
    }
    rendered.push_str(path.rsplit("::").next().unwrap_or(""));
    rendered
}