Skip to main content

layover_http/
lib.rs

1//! The Layover Tower HTTP surface.
2//!
3//! Everything in [`generated`] comes from `api/openapi.yaml` by way of
4//! `cargo xtask generate-api`. The specification is the contract: `cargo xtask verify`
5//! regenerates the module and fails if it differs, so the server cannot drift away from the
6//! document that describes it.
7//!
8//! This crate provides the *shape* of the API and nothing behind it. Implement [`Api`] to serve
9//! it; the Tower will, once it exists.
10//!
11//! ```no_run
12//! # use std::sync::Arc;
13//! # use layover_http::{Api, Health, Problem, Status, router};
14//! struct Stub;
15//!
16//! impl Api for Stub {
17//!     async fn get_health(&self) -> Result<Health, Problem> {
18//!         Ok(Health {
19//!             status: Status::Ok,
20//!             version: env!("CARGO_PKG_VERSION").to_owned(),
21//!             ground_stop: false,
22//!         })
23//!     }
24//!     # async fn list_agents(&self) -> Result<layover_http::AgentList, Problem> { todo!() }
25//!     # async fn list_pipelines(&self) -> Result<layover_http::PipelineList, Problem> { todo!() }
26//!     # async fn get_graph(&self, _: layover_http::GetGraphQuery) -> Result<layover_http::RouteMap, Problem> { todo!() }
27//!     # async fn list_help(&self, _: layover_http::ListHelpQuery) -> Result<layover_http::HelpList, Problem> { todo!() }
28//!     # async fn list_learnings(&self, _: layover_http::ListLearningsQuery) -> Result<layover_http::LearningList, Problem> { todo!() }
29//!     # async fn get_costs(&self, _: layover_http::GetCostsQuery) -> Result<layover_http::CostReport, Problem> { todo!() }
30//!     # async fn send_flight(&self, _: layover_http::SendFlightRequest) -> Result<layover_http::FlightAccepted, Problem> { todo!() }
31//!     # async fn list_runs(&self, _: layover_http::ListRunsQuery) -> Result<layover_http::RunList, Problem> { todo!() }
32//!     # async fn get_run(&self, _: layover_http::GetRunPath) -> Result<layover_http::Run, Problem> { todo!() }
33//!     # async fn stream_run(&self, _: layover_http::StreamRunPath) -> Result<layover_http::EventStream, Problem> { todo!() }
34//!     # async fn list_pending(&self) -> Result<layover_http::PendingList, Problem> { todo!() }
35//!     # async fn resolve_help(&self, _: layover_http::ResolveHelpRequest) -> Result<layover_http::HelpResolved, Problem> { todo!() }
36//!     # async fn judge_learning(&self, _: layover_http::JudgeLearningPath, _: layover_http::JudgeLearningRequest) -> Result<layover_http::Learning, Problem> { todo!() }
37//!     # async fn cancel_flight(&self, _: layover_http::CancelFlightPath) -> Result<layover_http::PendingList, Problem> { todo!() }
38//!     # async fn list_itineraries(&self, _: layover_http::ListItinerariesQuery) -> Result<layover_http::ItineraryList, Problem> { todo!() }
39//!     # async fn get_report(&self, _: layover_http::GetReportPath) -> Result<layover_http::Report, Problem> { todo!() }
40//!     # async fn engage_ground_stop(&self) -> Result<layover_http::GroundStop, Problem> { todo!() }
41//!     # async fn release_ground_stop(&self) -> Result<layover_http::GroundStop, Problem> { todo!() }
42//! }
43//!
44//! let app = router(Arc::new(Stub));
45//! ```
46
47pub mod generated;
48
49pub use generated::*;
50
51use axum::http::{StatusCode, header};
52use axum::response::{IntoResponse, Response};
53
54/// A server-sent event stream, returned by streaming operations.
55///
56/// Deliberately a thin wrapper over a body rather than a concrete stream type: the Tower decides
57/// how it produces events, and this crate only promises the content type the specification
58/// declares.
59pub struct EventStream(axum::body::Body);
60
61impl EventStream {
62    /// Wraps a body as an event stream.
63    #[must_use]
64    pub fn new(body: axum::body::Body) -> Self {
65        Self(body)
66    }
67}
68
69impl IntoResponse for EventStream {
70    fn into_response(self) -> Response {
71        (
72            StatusCode::OK,
73            [
74                (header::CONTENT_TYPE, "text/event-stream"),
75                (header::CACHE_CONTROL, "no-cache"),
76            ],
77            self.0,
78        )
79            .into_response()
80    }
81}
82
83impl Problem {
84    /// Builds a problem with a status and title.
85    #[must_use]
86    pub fn new(status: StatusCode, title: impl Into<String>) -> Self {
87        Self {
88            status: i32::from(status.as_u16()),
89            title: title.into(),
90            detail: None,
91        }
92    }
93
94    /// Adds explanatory detail.
95    #[must_use]
96    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
97        self.detail = Some(detail.into());
98        self
99    }
100
101    /// Returns the HTTP status this problem carries.
102    ///
103    /// Falls back to `500` when the numeric status is not a valid HTTP code, because a malformed
104    /// error must still produce a response rather than panicking inside a handler.
105    #[must_use]
106    pub fn status_code(&self) -> StatusCode {
107        u16::try_from(self.status)
108            .ok()
109            .and_then(|code| StatusCode::from_u16(code).ok())
110            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
111    }
112}
113
114impl IntoResponse for Problem {
115    fn into_response(self) -> Response {
116        (self.status_code(), axum::Json(self)).into_response()
117    }
118}