Skip to main content

appcore_api/
http.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: http.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/05/31 13:38:42 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 13:24:05 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Minimal HTTP host for runtime health, status, and command ingress.
12
13mod auth;
14mod command;
15mod handlers;
16mod query;
17mod reload;
18mod response;
19mod state;
20mod trace;
21
22pub use auth::{CommandTokenVerifier, HttpCommandAuth, RequestValidationDetails};
23pub use reload::{
24    HttpReloadPhase, HttpReloadPolicy, HttpReloadSnapshot, PreparedRuntimeHttpGeneration,
25    ReloadableRuntimeHttpHost, RuntimeHttpReloadError,
26};
27pub use state::{
28    CommandCapabilityPolicy, CommandCapabilityPolicyError, HttpApiConfig, RuntimeStaticInfo,
29    SyncLogView, SyncLogViewError,
30};
31
32#[cfg(test)]
33use crate::command_contract::{CommandRequest, CommandResponse, CommandResponseEvent};
34use crate::ApiRouter;
35use appcore_core::{RuntimeController, RuntimeOperationalMode};
36use axum::extract::DefaultBodyLimit;
37use axum::http::StatusCode;
38use axum::routing::{get, post};
39use axum::Router;
40use command::command_handler;
41use handlers::{
42    diagnostics_handler, health_handler, private_status_handler, public_status_handler,
43    status_handler,
44};
45use parking_lot::Mutex;
46use query::query_handler;
47use state::HttpState;
48use std::io;
49use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
50use std::sync::Arc;
51use std::time::Duration;
52
53/// Embedded HTTP host for stable Runtime health, status, command, and query routes.
54pub struct RuntimeHttpHost {
55    config: HttpApiConfig,
56    router: Router,
57}
58
59impl RuntimeHttpHost {
60    /// Creates a host exposing only static operational information.
61    pub fn new(config: HttpApiConfig, static_info: RuntimeStaticInfo) -> Self {
62        Self::with_runtime_state(config, static_info, RuntimeHttpStateParts::default())
63    }
64
65    /// Creates a host connected to a Runtime controller.
66    pub fn with_controller(
67        config: HttpApiConfig,
68        static_info: RuntimeStaticInfo,
69        controller: Arc<Mutex<RuntimeController>>,
70    ) -> Self {
71        Self::with_runtime_state(
72            config,
73            static_info,
74            RuntimeHttpStateParts {
75                controller: Some(controller),
76                ..RuntimeHttpStateParts::default()
77            },
78        )
79    }
80
81    /// Creates a host with controller, sync status, tick counter, and token policy.
82    pub fn with_runtime_state_and_auth(
83        config: HttpApiConfig,
84        static_info: RuntimeStaticInfo,
85        controller: Arc<Mutex<RuntimeController>>,
86        sync_log: Option<Arc<dyn SyncLogView>>,
87        tick_counter: Option<Arc<AtomicU64>>,
88        auth: HttpCommandAuth,
89    ) -> Self {
90        Self::with_runtime_state(
91            config,
92            static_info,
93            RuntimeHttpStateParts {
94                controller: Some(controller),
95                sync_log,
96                tick_counter,
97                auth,
98                ..RuntimeHttpStateParts::default()
99            },
100        )
101    }
102
103    /// Creates an authenticated host with a live operational-mode source.
104    pub fn with_runtime_state_auth_and_operation_mode(
105        config: HttpApiConfig,
106        static_info: RuntimeStaticInfo,
107        controller: Arc<Mutex<RuntimeController>>,
108        sync_log: Option<Arc<dyn SyncLogView>>,
109        tick_counter: Option<Arc<AtomicU64>>,
110        auth: HttpCommandAuth,
111        operation_mode: Arc<Mutex<appcore_core::RuntimeOperationalMode>>,
112    ) -> Self {
113        Self::with_runtime_state(
114            config,
115            static_info,
116            RuntimeHttpStateParts {
117                controller: Some(controller),
118                sync_log,
119                tick_counter,
120                operation_mode: Some(operation_mode),
121                auth,
122                ..RuntimeHttpStateParts::default()
123            },
124        )
125    }
126
127    /// Creates an authenticated host with application query routing.
128    pub fn with_runtime_state_auth_and_app_queries(
129        config: HttpApiConfig,
130        static_info: RuntimeStaticInfo,
131        controller: Arc<Mutex<RuntimeController>>,
132        sync_log: Option<Arc<dyn SyncLogView>>,
133        tick_counter: Option<Arc<AtomicU64>>,
134        auth: HttpCommandAuth,
135        app_query_router: Arc<Mutex<ApiRouter>>,
136    ) -> Self {
137        Self::with_runtime_state(
138            config,
139            static_info,
140            RuntimeHttpStateParts {
141                controller: Some(controller),
142                sync_log,
143                tick_counter,
144                app_query_router: Some(app_query_router),
145                auth,
146                ..RuntimeHttpStateParts::default()
147            },
148        )
149    }
150
151    /// Creates a host from the complete set of optional shared state parts.
152    pub fn with_state_parts(
153        config: HttpApiConfig,
154        static_info: RuntimeStaticInfo,
155        parts: RuntimeHttpStateParts,
156    ) -> Self {
157        Self::with_runtime_state(config, static_info, parts)
158    }
159
160    fn with_runtime_state(
161        config: HttpApiConfig,
162        static_info: RuntimeStaticInfo,
163        parts: RuntimeHttpStateParts,
164    ) -> Self {
165        if let Some(router) = &parts.app_query_router {
166            router.lock().freeze_queries();
167        }
168        let state = HttpState {
169            static_info,
170            controller: parts.controller,
171            app_query_router: parts.app_query_router,
172            sync_log: parts.sync_log,
173            tick_counter: parts.tick_counter,
174            operation_mode: parts.operation_mode,
175            command_policy: parts.command_policy,
176            supervisor: parts.supervisor,
177            auth: parts.auth,
178            max_payload_bytes: config.max_payload_bytes,
179            clock: Arc::new(appcore_core::SystemClock::new()),
180        };
181        let router = Router::new()
182            .route("/v1/health", get(health_handler))
183            .route("/v1/status", get(status_handler))
184            .route("/v1/status/public", get(public_status_handler))
185            .route("/v1/status/private", get(private_status_handler))
186            .route("/v1/diagnostics", get(diagnostics_handler))
187            .route("/v1/command", post(command_handler))
188            .route("/v1/query", post(query_handler))
189            .route("/health", get(update_required_handler))
190            .route("/status", get(update_required_handler))
191            .route("/status/public", get(update_required_handler))
192            .route("/status/private", get(update_required_handler))
193            .route("/diagnostics", get(update_required_handler))
194            .route("/command", post(update_required_handler))
195            .route("/query", post(update_required_handler))
196            .layer(DefaultBodyLimit::max(config.max_payload_bytes))
197            .with_state(state);
198        Self { config, router }
199    }
200
201    /// Returns immutable listener and payload-limit configuration.
202    pub fn config(&self) -> &HttpApiConfig {
203        &self.config
204    }
205
206    /// Returns an Axum router suitable for embedding in another listener.
207    pub fn router(&self) -> Router {
208        self.router.clone()
209    }
210
211    /// Runs the configured listener until `shutdown` becomes true.
212    pub fn run_until_shutdown(&self, shutdown: Arc<AtomicBool>) -> io::Result<()> {
213        if !self.config.enabled {
214            return Ok(());
215        }
216        let address = format!("{}:{}", self.config.host, self.config.port);
217        let router = self.router();
218        let runtime = tokio::runtime::Builder::new_current_thread()
219            .enable_all()
220            .build()
221            .map_err(io::Error::other)?;
222        runtime.block_on(async move {
223            let listener = tokio::net::TcpListener::bind(address).await?;
224            axum::serve(listener, router)
225                .with_graceful_shutdown(wait_for_shutdown(shutdown))
226                .await
227        })
228    }
229}
230
231async fn update_required_handler() -> (StatusCode, &'static str) {
232    (
233        StatusCode::UPGRADE_REQUIRED,
234        "NO MORE SUPPORTED PLEASE UPDATE",
235    )
236}
237
238#[derive(Default)]
239/// Optional shared state used to compose a [`RuntimeHttpHost`].
240pub struct RuntimeHttpStateParts {
241    /// Runtime controller used by command and operational query routes.
242    pub controller: Option<Arc<Mutex<RuntimeController>>>,
243    /// Read-only synchronization-log view used by status routes.
244    pub sync_log: Option<Arc<dyn SyncLogView>>,
245    /// Runtime tick counter exposed through diagnostics.
246    pub tick_counter: Option<Arc<AtomicU64>>,
247    /// Application-owned query router.
248    pub app_query_router: Option<Arc<Mutex<ApiRouter>>>,
249    /// Live operational mode used to gate writes.
250    pub operation_mode: Option<Arc<Mutex<RuntimeOperationalMode>>>,
251    /// Capability and leadership authorization policy for commands.
252    pub command_policy: Option<Arc<dyn CommandCapabilityPolicy>>,
253    /// Managed-service supervisor exposed through private diagnostics.
254    pub supervisor: Option<appcore_supervisor::Supervisor>,
255    /// Bearer-token authorization configuration.
256    pub auth: HttpCommandAuth,
257}
258
259async fn wait_for_shutdown(shutdown: Arc<AtomicBool>) {
260    loop {
261        if shutdown.load(Ordering::SeqCst) {
262            break;
263        }
264        tokio::time::sleep(Duration::from_millis(100)).await;
265    }
266}
267
268#[cfg(test)]
269mod http_tests;
270#[cfg(test)]
271mod reload_tests;