arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! The route lookup projection over the UAG.
//!
//! Finds routes by their dotted name in the flattened, de-duplicated route
//! table the UAG carries. Pure over `&Uag`; no filesystem access. Shared by
//! `arc inspect route <name>` and the MCP `inspect_route` tool so the
//! "resolve a route by name" logic exists exactly once.

use arcature_build::uag::Uag;
use arcature_build::uag::schema::RouteEntry;

/// Find every route whose dotted `name` equals `name`, in UAG order.
///
/// Returns borrowed references into the UAG (no allocation). Empty for an
/// unnamed route (`""`) — there is no useful "all unnamed routes" lookup, so
/// an empty `name` returns no matches. Routes are matched exactly by the
/// `name` field the `routes!` macro assigned.
pub(crate) fn find_routes_by_name<'u>(uag: &'u Uag, name: &str) -> Vec<&'u RouteEntry> {
    if name.is_empty() {
        return Vec::new();
    }
    uag.routes.iter().filter(|r| r.name == name).collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::inspection::tests::empty_module;
    use std::collections::BTreeMap;

    fn route(name: &str, method: &str, path: &str) -> RouteEntry {
        RouteEntry {
            method: method.into(),
            path: path.into(),
            name: name.into(),
            handler: format!("H::{name}"),
            pages: vec![],
            action_fields: vec![],
            action_type: String::new(),
            query_fields: vec![],
            query_type: String::new(),
            query_array: false,
            query_string_fields: vec![],
            query_string_type: String::new(),
        }
    }

    fn uag_with_routes(routes: Vec<RouteEntry>) -> Uag {
        Uag {
            schema_version: 1,
            application: "App".into(),
            framework_version: "2026.1.0".into(),
            modules: BTreeMap::from([("M".to_owned(), empty_module("M"))]),
            routes,
            services: vec![],
            pages: vec![],
        }
    }

    #[test]
    fn finds_a_unique_route_by_name() {
        let uag = uag_with_routes(vec![
            route("links.index", "get", "/links"),
            route("links.show", "get", "/links/{link}"),
        ]);
        let found = find_routes_by_name(&uag, "links.show");
        assert_eq!(found.len(), 1);
        assert_eq!(found[0].path, "/links/{link}");
        assert_eq!(found[0].method, "get");
    }

    #[test]
    fn finds_multiple_routes_sharing_a_name() {
        let uag = uag_with_routes(vec![route("dup", "get", "/a"), route("dup", "post", "/a")]);
        let found = find_routes_by_name(&uag, "dup");
        assert_eq!(found.len(), 2);
    }

    #[test]
    fn returns_empty_for_unknown_name() {
        let uag = uag_with_routes(vec![route("links.index", "get", "/links")]);
        assert!(find_routes_by_name(&uag, "nope").is_empty());
    }

    #[test]
    fn returns_empty_for_empty_name() {
        let uag = uag_with_routes(vec![route("", "get", "/links")]);
        assert!(find_routes_by_name(&uag, "").is_empty());
    }

    #[test]
    fn does_not_match_unnamed_route_even_when_path_matches() {
        let uag = uag_with_routes(vec![route("links.index", "get", "/links")]);
        // Empty name never matches, so an unnamed route can never be looked up
        // by accident — there is no ambiguous "all unnamed" projection.
        assert!(find_routes_by_name(&uag, "").is_empty());
    }
}