Skip to main content

pharos_runtime_core/
builtin.rs

1use crate::config::AppRuntime;
2use crate::stack::RuntimeStack;
3
4/// Response payload returned by a framework-owned route.
5#[derive(Debug, Clone, Eq, PartialEq)]
6pub struct FrameworkBuiltinResponse {
7    pub status_code: u16,
8    pub status_text: &'static str,
9    pub body: String,
10}
11
12/// Builds the response for a framework-owned runtime route.
13#[allow(clippy::too_many_arguments)]
14pub fn framework_builtin_response(
15    method: &str,
16    target: &str,
17    app: Option<&AppRuntime>,
18    stack: Option<&RuntimeStack>,
19    version: &str,
20    target_id: &str,
21    app_ready: impl Fn(&AppRuntime) -> bool,
22    stack_ready: impl Fn(&RuntimeStack) -> bool,
23) -> Option<FrameworkBuiltinResponse> {
24    let path_only = target.split('?').next().unwrap_or_default();
25    if !matches!(path_only, "/health" | "/live" | "/ready" | "/version") {
26        return None;
27    }
28    if !matches!(method, "GET" | "HEAD") {
29        return Some(FrameworkBuiltinResponse {
30            status_code: 405,
31            status_text: "Method Not Allowed",
32            body: "method not allowed\n".to_owned(),
33        });
34    }
35    match path_only {
36        "/version" => Some(FrameworkBuiltinResponse {
37            status_code: 200,
38            status_text: "OK",
39            body: format!("pharos {version} {target_id}\n"),
40        }),
41        "/health" => {
42            let ready = stack
43                .map(&stack_ready)
44                .or_else(|| app.map(&app_ready))
45                .unwrap_or(false);
46            let failed_app_count = stack.map(|value| value.failed_apps.len()).unwrap_or(0);
47            Some(FrameworkBuiltinResponse {
48                status_code: 200,
49                status_text: "OK",
50                body: format!(
51                    "server_status=green\nprobe=liveness\nreadiness_status={}\nfailed_app_count={}\n",
52                    if ready { "ready" } else { "not_ready" },
53                    failed_app_count
54                ),
55            })
56        }
57        "/live" => Some(FrameworkBuiltinResponse {
58            status_code: 200,
59            status_text: "OK",
60            body: "server_status=live\n".to_owned(),
61        }),
62        "/ready" => {
63            let ready = stack
64                .map(stack_ready)
65                .or_else(|| app.map(app_ready))
66                .unwrap_or(false);
67            Some(FrameworkBuiltinResponse {
68                status_code: if ready { 200 } else { 503 },
69                status_text: if ready { "OK" } else { "Service Unavailable" },
70                body: if ready {
71                    "server_status=ready\n".to_owned()
72                } else {
73                    "server_status=not_ready\n".to_owned()
74                },
75            })
76        }
77        _ => None,
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn health_distinguishes_live_from_ready() {
87        let stack = RuntimeStack::default();
88        let health = framework_builtin_response(
89            "GET",
90            "/health",
91            None,
92            Some(&stack),
93            "0.0.0",
94            "test-target",
95            |_| false,
96            |_| false,
97        )
98        .expect("health response");
99        assert_eq!(health.status_code, 200);
100        assert!(health.body.contains("readiness_status=not_ready"));
101
102        let ready = framework_builtin_response(
103            "GET",
104            "/ready",
105            None,
106            Some(&stack),
107            "0.0.0",
108            "test-target",
109            |_| false,
110            |_| false,
111        )
112        .expect("ready response");
113        assert_eq!(ready.status_code, 503);
114    }
115}