weavatrix-rust 2.16.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::{
    BTreeSet, NodeKind, RepositoryState, Value, http_evidence::SourceCache, http_route, json,
    optional_str, optional_u64,
};

pub(in crate::operations) fn http_contracts(
    backend: &RepositoryState,
    clients: &[(String, &RepositoryState)],
    args: &Value,
) -> Result<Value, String> {
    let method = optional_str(args, "method")?;
    let path_filter = optional_str(args, "path")?;
    let changed_files = changed_http_files(args)?;
    let max_endpoints = usize::try_from(optional_u64(args, "max_endpoints")?.unwrap_or(250))
        .map_err(|_| "max_endpoints is too large".to_owned())?
        .clamp(1, 500);
    let endpoints = backend
        .graph()
        .nodes()
        .iter()
        .enumerate()
        .filter(|(_, node)| node.kind == NodeKind::Endpoint)
        .filter(|(_, node)| !node.label.starts_with("GRAPHQL ") && !node.label.starts_with("GRPC "))
        .filter(|(_, node)| method.is_none_or(|method| node.label.starts_with(method)))
        .filter(|(_, node)| path_filter.is_none_or(|path| node.label.ends_with(path)))
        .filter(|(slot, _)| super::super::node_is_visible(backend, *slot, args))
        .filter(|(_, node)| {
            changed_files.is_empty()
                || node
                    .span
                    .as_ref()
                    .is_some_and(|span| changed_files.contains(&normalize_http_path(&span.file)))
        })
        .take(max_endpoints)
        .map(|(_, node)| node)
        .collect::<Vec<_>>();
    let per_item = usize::try_from(optional_u64(args, "per_item_limit")?.unwrap_or(5))
        .map_err(|_| "per_item_limit is too large".to_owned())?
        .clamp(1, 25);
    let max_matches = usize::try_from(optional_u64(args, "max_matches")?.unwrap_or(1_000))
        .map_err(|_| "max_matches is too large".to_owned())?
        .clamp(1, 5_000);
    let mut contracts = Vec::new();
    let mut total_calls = 0_usize;
    let mut method_mismatches = 0_usize;
    let mut unmatched = 0_usize;
    for endpoint in &endpoints {
        let (backend_method, route) = endpoint
            .label
            .split_once(' ')
            .unwrap_or(("ANY", endpoint.label.as_str()));
        let query = http_route::route_query(route);
        if query.is_empty() {
            // A root route has no selective literal to search for. Passing an
            // empty query to the search engine is a tool error; searching for
            // "/" would instead manufacture thousands of unrelated matches.
            // Keep it as an explicit unmatched endpoint.
            unmatched += 1;
            continue;
        }
        let calls =
            find_endpoint_calls(route, backend_method, clients, args, per_item, max_matches)?;
        method_mismatches += calls.method_mismatches;
        total_calls += calls.total;
        if calls.affected_files.is_empty() {
            unmatched += 1;
        } else {
            contracts.push(json!({
                "key": format!("http:{}:{route}", backend_method),
                "transport": "http",
                "method": backend_method,
                "path": route,
                "backend_endpoint": endpoint,
                "callsites": calls.callsites,
                "affected_files": calls.affected_files,
                "callsite_count": calls.total
            }));
        }
    }
    Ok(json!({
        "status": "COMPLETE",
        "totals": {
            "endpoints": endpoints.len(),
            "matches": contracts.len(),
            "callsites": total_calls,
            "method_mismatches": method_mismatches,
            "unmatched_endpoints": unmatched
        },
        "contracts": contracts
    }))
}

struct EndpointCalls {
    callsites: Vec<Value>,
    affected_files: BTreeSet<String>,
    total: usize,
    method_mismatches: usize,
}

fn changed_http_files(args: &Value) -> Result<BTreeSet<String>, String> {
    args.get("changed_files")
        .map(|value| {
            value
                .as_array()
                .ok_or_else(|| "changed_files must be an array of strings".to_owned())?
                .iter()
                .map(|item| {
                    item.as_str()
                        .map(normalize_http_path)
                        .ok_or_else(|| "changed_files must contain only strings".to_owned())
                })
                .collect()
        })
        .transpose()
        .map(Option::unwrap_or_default)
}

fn find_endpoint_calls(
    route: &str,
    backend_method: &str,
    clients: &[(String, &RepositoryState)],
    args: &Value,
    per_item: usize,
    max_matches: usize,
) -> Result<EndpointCalls, String> {
    let mut calls = EndpointCalls {
        callsites: Vec::new(),
        affected_files: BTreeSet::new(),
        total: 0,
        method_mismatches: 0,
    };
    for (client, client_state) in clients {
        let result = super::super::source::search(
            client_state,
            &json!({"query": http_route::route_query(route), "max_results": max_matches}),
        )?;
        let mut cache = SourceCache::new();
        for evidence in result["matches"]
            .as_array()
            .into_iter()
            .flatten()
            .filter(|evidence| {
                evidence["path"]
                    .as_str()
                    .is_some_and(|path| super::super::health::path_is_visible(path, args))
            })
            .filter(|evidence| {
                evidence["text"]
                    .as_str()
                    .is_some_and(|line| http_route::route_matches(route, line))
            })
        {
            let Some(path) = evidence["path"].as_str() else {
                continue;
            };
            let Some(line) = evidence["line"]
                .as_u64()
                .and_then(|value| u32::try_from(value).ok())
            else {
                continue;
            };
            // Text search only nominates candidates. Proven external callsites
            // require token evidence of an HTTP call with a literal URL.
            let Some(callsite) = cache.resolve_callsite(client_state, path, line, route) else {
                continue;
            };
            let mismatch = callsite.method.mismatches(backend_method);
            calls.method_mismatches += usize::from(mismatch);
            calls.total += 1;
            calls.affected_files.insert(path.to_owned());
            if calls.callsites.len() < per_item {
                calls.callsites.push(json!({
                    "client": client,
                    "file": path,
                    "line": callsite.line,
                    "method": callsite.method.as_json(),
                    "method_mismatch": mismatch,
                    "match": if callsite.exact_literal {
                        "EXACT_LITERAL"
                    } else {
                        "TEMPLATE_PREFIX"
                    },
                    "text": callsite.evidence
                }));
            }
        }
    }
    Ok(calls)
}

fn normalize_http_path(path: &str) -> String {
    path.replace('\\', "/").trim_start_matches("./").to_owned()
}