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}