Skip to main content

termivar_api/
lib.rs

1//! Experimental HTTP adapter for Termivar.
2//!
3//! ## Runtime scope
4//!
5//! - **Build:** separate workspace crate (`termivar-api`).
6//! - **Execution:** optional CLI startup hook (`termivar-cli/api-adapter`).
7//!   `start_api` fails nonzero and does not bind a listener; `router` exposes only
8//!   `GET /health` as a library value.
9//! - **Default `termivar scan`:** no.
10//! - **Support:** unsupported — no live network listener.
11//!
12//! See `docs/internals/runtime-map.md`.
13//!
14//! The implemented alpha surface is deliberately small: [`router`] exposes
15//! `GET /health`, while [`start_api`] is a startup hook and does not yet bind a
16//! network listener.
17//!
18//! # Example
19//!
20//! ```rust
21//! let app = termivar_api::router();
22//! # let _ = app;
23//! ```
24
25#![deny(rustdoc::broken_intra_doc_links)]
26
27use std::fmt;
28
29use axum::{routing::get, Router};
30
31/// Error returned by the unsupported listener startup hook.
32#[derive(Clone, PartialEq, Eq)]
33pub struct ApiError {
34    address: String,
35}
36
37impl ApiError {
38    fn unsupported_listener(address: impl Into<String>) -> Self {
39        Self {
40            address: address.into(),
41        }
42    }
43
44    /// Returns the address that was not bound.
45    pub fn address(&self) -> &str {
46        &self.address
47    }
48}
49
50impl fmt::Display for ApiError {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(
53            formatter,
54            "the API listener adapter is unsupported and did not bind {}",
55            self.address
56        )
57    }
58}
59
60impl fmt::Debug for ApiError {
61    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62        fmt::Display::fmt(self, formatter)
63    }
64}
65
66impl std::error::Error for ApiError {}
67
68/// Result returned by the API adapter boundary.
69pub type Result<T> = std::result::Result<T, ApiError>;
70
71/// Returns `OK` for process-level health checks.
72pub async fn health() -> &'static str {
73    "OK"
74}
75
76/// Builds the currently implemented Axum router.
77///
78/// The alpha router contains only `GET /health`.
79pub fn router() -> Router {
80    Router::new().route("/health", get(health))
81}
82
83/// Rejects the unsupported API startup hook.
84///
85/// This function deliberately returns an error because it does not bind `addr`.
86/// Callers that need a live server may serve [`router`] with their own Tokio
87/// listener until the transport lifecycle is stabilized.
88pub async fn start_api(addr: &str) -> Result<()> {
89    Err(ApiError::unsupported_listener(addr))
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[tokio::test]
97    async fn unsupported_startup_fails_closed() {
98        let error = start_api("127.0.0.1:8080").await.unwrap_err();
99        assert_eq!(error.address(), "127.0.0.1:8080");
100        assert!(error.to_string().contains("unsupported"));
101        assert!(error.to_string().contains("did not bind"));
102        assert_eq!(format!("{error:?}"), error.to_string());
103    }
104}