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}