Skip to main content

alux_http_rocket/
route.rs

1//! Composes selectors, and builds the Rocket routes they state.
2
3use crate::{RocketAnswer, RocketBody, RocketRequest};
4use alux_http::{
5    HttpMethod, HttpSelectorAlg, PathSegment, PathSyntaxAlg, RouteAlg, RoutePath, SelectorAlg, compose_path,
6    describe_path,
7};
8use bytes::Bytes;
9use core::future::Future;
10use core::pin::Pin;
11use futures::TryStreamExt;
12use rocket::data::{ByteUnit, Data};
13use rocket::http::{HeaderMap, Method, Status};
14use rocket::response::Response;
15use rocket::route::{Handler, Outcome, Route};
16use rocket::{Build, Request, Rocket, async_trait};
17use std::io::Cursor;
18use std::sync::Arc;
19use tokio_util::io::StreamReader;
20
21/// How many bytes of a request body are read before the request is refused.
22///
23/// Rocket hands a body over capped rather than whole, so a limit is stated here rather than
24/// inherited. A service accepting larger uploads states its own by mounting a data limit in its
25/// `rocket::Config`, which is where production sets one.
26const READS: ByteUnit = ByteUnit::Mebibyte(8);
27
28/// The status a body larger than what is read is answered with.
29const TOO_LARGE: Status = Status { code: 413 };
30
31/// The status a body that could not be read at all is answered with.
32const UNREADABLE: Status = Status { code: 400 };
33
34/// Spells route parameters the way Rocket's router reads them.
35struct RocketPath;
36
37impl PathSyntaxAlg for RocketPath {
38    fn param(&self, name: &str) -> String {
39        format!("<{name}>")
40    }
41
42    fn tail(&self, name: &str) -> String {
43        format!("<{name}..>")
44    }
45}
46
47/// Interprets a request method as the one Rocket routes on.
48fn rocket_method(method: HttpMethod) -> Method {
49    match method {
50        HttpMethod::Get => Method::Get,
51        HttpMethod::Post => Method::Post,
52        HttpMethod::Put => Method::Put,
53        HttpMethod::Patch => Method::Patch,
54        HttpMethod::Delete => Method::Delete,
55        HttpMethod::Head => Method::Head,
56        HttpMethod::Options => Method::Options,
57        HttpMethod::Trace => Method::Trace,
58        HttpMethod::Connect => Method::Connect,
59    }
60}
61
62/// The answer one reached endpoint produces.
63pub type Answer = Pin<Box<dyn Future<Output = RocketAnswer> + Send>>;
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66enum RocketSelectorPart {
67    Method(HttpMethod),
68    Path(RoutePath),
69    Prefix(RoutePath),
70}
71
72/// Carries route-selection meaning before it is mounted on Rocket.
73#[derive(Debug, Clone, Default, PartialEq, Eq)]
74pub struct RocketSelector {
75    parts: Vec<RocketSelectorPart>,
76}
77
78impl RocketSelector {
79    /// Returns the composed absolute path this selector matches.
80    pub fn path(&self) -> String {
81        describe_path(self.paths())
82    }
83
84    /// Returns the composed path in the spelling Rocket's router reads.
85    pub(crate) fn rocket_path(&self) -> String {
86        compose_path(self.paths(), &RocketPath)
87    }
88
89    fn paths(&self) -> impl Iterator<Item = &RoutePath> {
90        self.parts.iter().filter_map(|part| match part {
91            RocketSelectorPart::Path(path) | RocketSelectorPart::Prefix(path) => Some(path),
92            RocketSelectorPart::Method(_) => None,
93        })
94    }
95
96    fn segments(&self) -> Vec<PathSegment> {
97        self.paths().flat_map(|path| path.segments().iter().cloned()).collect()
98    }
99
100    /// Returns the selected method and path, using `*` when no method is selected.
101    pub fn label(&self) -> String {
102        let method = self.method().map_or("*", HttpMethod::label);
103        format!("{method} {}", self.path())
104    }
105
106    pub(crate) fn method(&self) -> Option<HttpMethod> {
107        self.parts.iter().rev().find_map(|part| match part {
108            RocketSelectorPart::Method(method) => Some(*method),
109            RocketSelectorPart::Path(_) | RocketSelectorPart::Prefix(_) => None,
110        })
111    }
112
113    /// Returns what the path bound, read out of the segments Rocket routed.
114    fn captures(&self, request: &Request<'_>) -> Vec<String> {
115        let mut captured = Vec::new();
116        for (index, segment) in self.segments().iter().enumerate() {
117            match segment {
118                PathSegment::Literal(_) => {}
119                PathSegment::Param(_) => {
120                    captured.push(request.routed_segment(index).unwrap_or_default().to_owned());
121                }
122                PathSegment::Tail(_) => {
123                    let tail = request.routed_segments(index..).collect::<Vec<_>>().join("/");
124                    captured.push(tail);
125                }
126            }
127        }
128
129        captured
130    }
131}
132
133/// Erases what one endpoint does with a request Rocket routed to it.
134#[derive(Clone)]
135pub struct RocketEndpoint(Arc<dyn Fn(RocketRequest) -> Answer + Send + Sync>);
136
137impl RocketEndpoint {
138    /// States an endpoint as what it answers, given what Rocket routed.
139    pub fn new<Reach>(reach: Reach) -> Self
140    where
141        Reach: Fn(RocketRequest) -> Answer + Send + Sync + 'static,
142    {
143        Self(Arc::new(reach))
144    }
145}
146
147#[derive(Clone)]
148struct RocketRouteEntry {
149    selector: RocketSelector,
150    endpoint: RocketEndpoint,
151}
152
153/// Carries a composable collection of Rocket endpoints.
154#[derive(Clone, Default)]
155pub struct RocketRoute {
156    entries: Vec<RocketRouteEntry>,
157}
158
159impl RocketRoute {
160    /// Returns each composed selector as `METHOD /path`, in declaration order.
161    pub fn labels(&self) -> Vec<String> {
162        self.entries.iter().map(|entry| entry.selector.label()).collect()
163    }
164
165    /// Returns each composed route path, in declaration order.
166    pub fn paths(&self) -> Vec<String> {
167        self.entries.iter().map(|entry| entry.selector.path()).collect()
168    }
169
170    /// Materializes the composed meaning as native Rocket routes.
171    ///
172    /// Rocket states a method on every route, so an endpoint composed without one states nothing it
173    /// can mount.
174    pub fn into_rocket(self) -> Vec<Route> {
175        self.entries
176            .into_iter()
177            .filter_map(|entry| {
178                let method = rocket_method(entry.selector.method()?);
179                let path = entry.selector.rocket_path();
180                let reaching = Reaching { selector: entry.selector, endpoint: entry.endpoint };
181
182                Some(Route::new(method, &path, reaching))
183            })
184            .collect()
185    }
186
187    /// Mounts every route this composition states on a Rocket instance.
188    pub fn mount(self, rocket: Rocket<Build>) -> Rocket<Build> {
189        rocket.mount("/", self.into_rocket())
190    }
191}
192
193/// What one endpoint does with a request Rocket routed to it.
194#[derive(Clone)]
195struct Reaching {
196    selector: RocketSelector,
197    endpoint: RocketEndpoint,
198}
199
200#[async_trait]
201impl Handler for Reaching {
202    async fn handle<'r>(&self, request: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
203        let captures = self.selector.captures(request);
204        let query = request.uri().query().map(|query| query.as_str().to_owned()).unwrap_or_default();
205        let mut headers = HeaderMap::new();
206        for header in request.headers().iter() {
207            headers.add_raw(header.name().to_string(), header.value().to_string());
208        }
209        let body = match data.open(READS).into_bytes().await {
210            // A body larger than what is read would reach the endpoint as a different, well formed
211            // request, so it is refused rather than truncated.
212            Ok(read) if !read.is_complete() => {
213                return Outcome::Success(refused(TOO_LARGE, "the body is larger than this service reads"));
214            }
215            Ok(read) => read.into_inner(),
216            Err(error) => {
217                return Outcome::Success(refused(UNREADABLE, &format!("the body could not be read: {error}")));
218            }
219        };
220        let answered = (self.endpoint.0)(RocketRequest { captures, query, headers, body }).await;
221
222        Outcome::Success(respond(answered))
223    }
224}
225
226/// Builds the response Rocket answers with from what an endpoint stated.
227/// Answers a request whose body this service will not read, saying which it is.
228fn refused<'r>(status: Status, message: &str) -> Response<'r> {
229    let said = message.to_owned();
230    let mut response = Response::build();
231    response.status(status);
232    response.raw_header("content-type", "text/plain; charset=utf-8");
233    response.sized_body(said.len(), Cursor::new(said));
234
235    response.finalize()
236}
237
238fn respond<'r>(answered: RocketAnswer) -> Response<'r> {
239    let mut response = Response::build();
240    response.status(Status::new(answered.status.code()));
241    for (name, value) in answered.headers {
242        response.raw_header(name, value);
243    }
244    match answered.body {
245        RocketBody::Stated(body) => response.sized_body(body.len(), Cursor::new(body)),
246        // Rocket reads a body it streams, so what produces the chunks is read as one.
247        RocketBody::Produced(chunks) => response.streamed_body(StreamReader::new(chunks.map_ok(Bytes::from))),
248    };
249
250    response.finalize()
251}
252
253/// Interprets categorical route composition as native Rocket routing.
254#[derive(Debug, Default)]
255pub struct RocketRouteImpl;
256
257impl SelectorAlg for RocketRouteImpl {
258    type Selector = RocketSelector;
259
260    fn identity(&self) -> RocketSelector {
261        RocketSelector::default()
262    }
263
264    fn compose(&self, mut first: RocketSelector, second: RocketSelector) -> RocketSelector {
265        first.parts.extend(second.parts);
266        first
267    }
268}
269
270impl RouteAlg for RocketRouteImpl {
271    type Route = RocketRoute;
272    type Selector = RocketSelector;
273    type Endpoint = RocketEndpoint;
274
275    fn initial(&self) -> RocketRoute {
276        RocketRoute::default()
277    }
278
279    fn coproduct(&self, mut left: RocketRoute, right: RocketRoute) -> RocketRoute {
280        left.entries.extend(right.entries);
281        left
282    }
283
284    fn precompose(&self, selector: RocketSelector, mut route: RocketRoute) -> RocketRoute {
285        for entry in &mut route.entries {
286            entry.selector = self.compose(selector.clone(), core::mem::take(&mut entry.selector));
287        }
288        route
289    }
290
291    fn lift(&self, endpoint: RocketEndpoint) -> RocketRoute {
292        RocketRoute { entries: vec![RocketRouteEntry { selector: self.identity(), endpoint }] }
293    }
294}
295
296impl HttpSelectorAlg for RocketRouteImpl {
297    type Selector = RocketSelector;
298
299    fn http_method(&self, method: HttpMethod) -> RocketSelector {
300        RocketSelector { parts: vec![RocketSelectorPart::Method(method)] }
301    }
302
303    fn http_path(&self, path: &RoutePath) -> RocketSelector {
304        RocketSelector { parts: vec![RocketSelectorPart::Path(path.clone())] }
305    }
306
307    fn http_prefix(&self, prefix: &RoutePath) -> RocketSelector {
308        RocketSelector { parts: vec![RocketSelectorPart::Prefix(prefix.clone())] }
309    }
310}