weavatrix-rust 2.1.1

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::kinds::{node_kind, span};
use crate::language::{DomainFact, FileFacts, MountFact, SymbolLocator};
use std::collections::BTreeMap;
use weavatrix_graph::{EdgeKind, NodeKind};
use weavatrix_parse::{DeclarationKind, Facts, ReferenceKind};

pub(super) fn class_route_prefixes(facts: &Facts) -> BTreeMap<String, String> {
    let mut prefixes = BTreeMap::new();
    for annotation in facts.references.iter().filter(|reference| {
        reference.kind == ReferenceKind::Call
            && reference.name == "RequestMapping"
            && reference.owner.is_none()
    }) {
        let Some(prefix) = annotation.string_arguments.first() else {
            continue;
        };
        let Some(class) = facts
            .declarations
            .iter()
            .filter(|declaration| {
                declaration.owner.is_none()
                    && matches!(
                        declaration.kind,
                        DeclarationKind::Class | DeclarationKind::Struct
                    )
                    && declaration.span.start > annotation.span.end
            })
            .min_by_key(|declaration| declaration.span.start)
        else {
            continue;
        };
        prefixes.insert(class.name.clone(), normalize_route(prefix));
    }
    prefixes
}

const ROUTES: &[(&str, &str, bool)] = &[
    ("get", "GET", true),
    ("post", "POST", true),
    ("put", "PUT", true),
    ("patch", "PATCH", true),
    ("delete", "DELETE", true),
    ("head", "HEAD", true),
    ("options", "OPTIONS", true),
    ("all", "ANY", true),
    ("use", "ANY", true),
    ("route", "ANY", true),
    ("GET", "GET", false),
    ("POST", "POST", false),
    ("PUT", "PUT", false),
    ("PATCH", "PATCH", false),
    ("DELETE", "DELETE", false),
    ("HEAD", "HEAD", false),
    ("OPTIONS", "OPTIONS", false),
    ("ALL", "ANY", false),
    ("HandleFunc", "ANY", false),
    ("Handle", "ANY", false),
    ("RequestMapping", "ANY", false),
    ("GetMapping", "GET", false),
    ("PostMapping", "POST", false),
    ("PutMapping", "PUT", false),
    ("PatchMapping", "PATCH", false),
    ("DeleteMapping", "DELETE", false),
    ("HttpGet", "GET", false),
    ("HttpPost", "POST", false),
    ("HttpPut", "PUT", false),
    ("HttpPatch", "PATCH", false),
    ("HttpDelete", "DELETE", false),
];

pub(super) fn domain(
    reference: &weavatrix_parse::Reference,
    path: &str,
    facts: &Facts,
    class_route_prefixes: &BTreeMap<String, String>,
    converted: &mut FileFacts,
) {
    if matches!(reference.kind, ReferenceKind::Reads | ReferenceKind::Writes) {
        converted.domains.push(DomainFact {
            name: reference.name.clone(),
            kind: NodeKind::Table,
            relation: if reference.kind == ReferenceKind::Writes {
                EdgeKind::Writes
            } else {
                EdgeKind::Reads
            },
            span: span(&reference.span, path),
            owner: None,
        });
        return;
    }
    if reference.kind != ReferenceKind::Call {
        return;
    }
    let name = reference.name.as_str();
    let first = reference.string_arguments.first();
    let owner = || {
        reference.owner.as_ref().and_then(|owner| {
            facts
                .declarations
                .iter()
                .find(|declaration| declaration.name == *owner)
                .map(|declaration| SymbolLocator {
                    name: declaration.name.clone(),
                    kind: node_kind(declaration.kind),
                    span: span(&declaration.span, path),
                })
        })
    };

    if name == "use"
        && reference.receiver.is_some()
        && let Some(binding) = reference.name_arguments.first()
        && let Some(target) = facts
            .imports
            .iter()
            .find(|import| import.names.iter().any(|name| name == binding))
    {
        converted.mounts.push(MountFact {
            prefix: first.cloned().unwrap_or_default(),
            target: target.specifier.clone(),
        });
    }

    let Some(argument) = first else {
        return;
    };
    if let Some(route) = route_fact(reference, argument, path, class_route_prefixes, owner()) {
        converted.domains.push(route);
        return;
    }

    let (kind, relation) = match name {
        "topic" | "publish" => (NodeKind::Topic, EdgeKind::Publishes),
        "subscribe" | "consume" => (NodeKind::Topic, EdgeKind::Consumes),
        "queue_declare" | "queueDeclare" | "assertQueue" => (NodeKind::Queue, EdgeKind::Configures),
        "exchange_declare" | "exchangeDeclare" | "assertExchange" => {
            (NodeKind::Exchange, EdgeKind::Configures)
        }
        "collection" | "getCollection" => (NodeKind::Collection, EdgeKind::Reads),
        _ => return,
    };
    converted.domains.push(DomainFact {
        name: argument.clone(),
        kind,
        relation,
        span: span(&reference.span, path),
        owner: owner(),
    });
}

fn route_fact(
    reference: &weavatrix_parse::Reference,
    argument: &str,
    path: &str,
    class_route_prefixes: &BTreeMap<String, String>,
    owner: Option<SymbolLocator>,
) -> Option<DomainFact> {
    let name = reference.name.as_str();
    if name == "RequestMapping" && reference.owner.is_none() {
        return None;
    }
    let annotation_route = matches!(
        name,
        "RequestMapping"
            | "GetMapping"
            | "PostMapping"
            | "PutMapping"
            | "PatchMapping"
            | "DeleteMapping"
            | "HttpGet"
            | "HttpPost"
            | "HttpPut"
            | "HttpPatch"
            | "HttpDelete"
    );
    let (_, method, _) = ROUTES.iter().find(|(call, _, needs_receiver)| {
        *call == name && (!needs_receiver || reference.receiver.is_some())
    })?;
    if !argument.starts_with('/') && !annotation_route {
        return None;
    }
    let route = reference
        .owner
        .as_ref()
        .and_then(|owner| class_route_prefixes.get(owner))
        .map_or_else(
            || normalize_route(argument),
            |prefix| join_routes(prefix, argument),
        );
    Some(DomainFact {
        name: format!("{method} {route}"),
        kind: NodeKind::Endpoint,
        relation: EdgeKind::Exposes,
        span: span(&reference.span, path),
        owner,
    })
}

fn normalize_route(route: &str) -> String {
    let trimmed = route.trim();
    if trimmed.is_empty() || trimmed == "/" {
        "/".to_owned()
    } else {
        format!("/{}", trimmed.trim_matches('/'))
    }
}

fn join_routes(prefix: &str, route: &str) -> String {
    let prefix = normalize_route(prefix);
    let route = normalize_route(route);
    if prefix == "/" {
        route
    } else if route == "/" {
        prefix
    } else {
        format!("{prefix}{route}")
    }
}