Skip to main content

pharos_runtime_core/
route.rs

1use serde_json::Value;
2
3use crate::config::AppRuntime;
4use crate::stack::{RuntimeAppLoadFailure, RuntimeStack};
5
6/// Joins an application mount and route path into one owned HTTP namespace.
7pub fn mounted_route_pattern(mount: &str, route: &str) -> String {
8    let mount = mount.trim_end_matches('/');
9    let route = route.trim_start_matches('/');
10    ownership_route_pattern(&format!("{mount}/{route}"))
11}
12
13/// Normalizes dynamic route parameters for deterministic collision comparison.
14pub fn ownership_route_pattern(path: &str) -> String {
15    path.split('/')
16        .map(|segment| {
17            if segment.starts_with(':') || (segment.starts_with('{') && segment.ends_with('}')) {
18                ":parameter"
19            } else {
20                segment
21            }
22        })
23        .collect::<Vec<_>>()
24        .join("/")
25}
26
27/// Performs route pattern matches.
28pub fn route_pattern_matches(pattern: &str, request_path: &str) -> bool {
29    let pattern_segments: Vec<&str> = pattern.trim_matches('/').split('/').collect();
30    let request_segments: Vec<&str> = request_path.trim_matches('/').split('/').collect();
31    if pattern == "/" && request_path == "/" {
32        return true;
33    }
34    pattern_segments.len() == request_segments.len()
35        && pattern_segments
36            .iter()
37            .zip(request_segments)
38            .all(|(pattern, actual)| {
39                (pattern.starts_with(':') || (pattern.starts_with('{') && pattern.ends_with('}')))
40                    || pattern == &actual
41            })
42}
43
44/// Resolves the most specific declarative route for a request method and path.
45pub fn matched_app_route_value(app: &AppRuntime, method: &str, path: &str) -> Option<Value> {
46    let ir = app.application_ir.as_ref()?;
47    let segment_count = path
48        .trim_matches('/')
49        .split('/')
50        .filter(|segment| !segment.is_empty())
51        .count();
52    let page_key = ("page".to_owned(), segment_count);
53    let action_key = ("action".to_owned(), segment_count);
54    let page_routes = ir.route_buckets.get(&page_key).into_iter().flatten();
55    let action_routes = ir.route_buckets.get(&action_key).into_iter().flatten();
56    page_routes
57        .chain(action_routes)
58        .filter_map(|id| ir.routes.get(id))
59        .filter(|route| {
60            route.method == method
61                || (method == "HEAD" && route.method == "GET")
62                || (matches!(method, "GET" | "HEAD" | "POST")
63                    && route.action_id.as_ref().is_some_and(|action_id| {
64                        action_id.as_str() == "architect_view_generated_app"
65                    }))
66        })
67        .filter(|route| route_pattern_matches(&route.path, path))
68        .max_by_key(|route| {
69            let pattern = route.path.as_str();
70            (
71                pattern == path,
72                pattern
73                    .split('/')
74                    .filter(|segment| !segment.is_empty() && !segment.starts_with(':'))
75                    .count(),
76                pattern.len(),
77            )
78        })
79        .map(|route| {
80            let mut view = route.compatibility_view.clone();
81            if let Some(object) = view.as_object_mut() {
82                object.insert(
83                    "effective_security".to_owned(),
84                    serde_json::json!({
85                        "csrf_mode": route.security.csrf_mode,
86                        "trusted_origin_policy": route.security.trusted_origin_policy,
87                        "tenant_scope": route.security.tenant_scope,
88                        "rate_limit_policy": route.security.rate_limit_policy,
89                        "capability_policy": route.security.capability_policy,
90                    }),
91                );
92            }
93            view
94        })
95}
96
97/// Finds the failed app load record that owns a request path.
98pub fn matched_failed_app<'a>(
99    stack: &'a RuntimeStack,
100    best_mount_index: impl FnOnce(Vec<(usize, &'a str)>) -> Option<usize>,
101) -> Option<&'a RuntimeAppLoadFailure> {
102    let candidates = stack
103        .failed_apps
104        .iter()
105        .enumerate()
106        .filter_map(|(index, app)| {
107            (!app.mount_path.is_empty()).then_some((index, app.mount_path.as_str()))
108        })
109        .collect::<Vec<_>>();
110    best_mount_index(candidates).and_then(|index| stack.failed_apps.get(index))
111}
112
113#[cfg(test)]
114mod tests {
115    use super::{matched_failed_app, mounted_route_pattern, route_pattern_matches};
116    use crate::stack::{RuntimeAppLoadFailure, RuntimeStack};
117
118    #[test]
119    fn route_patterns_match_named_and_braced_segments() {
120        assert!(route_pattern_matches("/businesses/:id/", "/businesses/7/"));
121        assert!(route_pattern_matches("/orders/{id}/", "/orders/abc/"));
122        assert!(!route_pattern_matches(
123            "/orders/{id}/",
124            "/orders/abc/items/"
125        ));
126        assert_eq!(
127            mounted_route_pattern("/shop/", "/orders/:id/"),
128            "/shop/orders/:parameter/"
129        );
130    }
131
132    #[test]
133    fn failed_app_lookup_uses_mount_path() {
134        let stack = RuntimeStack {
135            failed_apps: vec![
136                RuntimeAppLoadFailure {
137                    app_id: "docs".to_owned(),
138                    mount_path: "/docs".to_owned(),
139                    app_root: "apps/docs".to_owned(),
140                    artifact_dir: "dist/apps/docs".to_owned(),
141                    error: "missing artifact".to_owned(),
142                },
143                RuntimeAppLoadFailure {
144                    app_id: "docs_admin".to_owned(),
145                    mount_path: "/docs/admin".to_owned(),
146                    app_root: "apps/docs_admin".to_owned(),
147                    artifact_dir: "dist/apps/docs_admin".to_owned(),
148                    error: "missing artifact".to_owned(),
149                },
150            ],
151            ..RuntimeStack::default()
152        };
153        let matched = matched_failed_app(&stack, |candidates| {
154            crate::best_mount_index("/docs/admin/panel", candidates)
155        })
156        .expect("failed app match");
157        assert_eq!(matched.app_id, "docs_admin");
158    }
159}