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