1use crate::engine::InstantMillis;
2use crate::identity::IdentityHash;
3use crate::routing::links::request::{
4 packed_binary_len, write_packed_binary_header, RequestId, MAX_PACKED_BINARY_HEADER_LEN,
5};
6use crate::routing::links::LinkId;
7use crate::routing::request_handlers::{RequestPathHash, RequestPolicy};
8use crate::units::RttMillis;
9use crate::wire::DestinationHash;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RequestEndpointPolicy {
13 AllowNone,
14 AllowAll,
15 AllowList(&'static [IdentityHash]),
16}
17
18impl RequestEndpointPolicy {
19 #[must_use]
20 pub fn engine_policy(self) -> RequestPolicy {
21 match self {
22 RequestEndpointPolicy::AllowNone => RequestPolicy::AllowNone,
23 RequestEndpointPolicy::AllowAll => RequestPolicy::AllowAll,
24 RequestEndpointPolicy::AllowList(_) => RequestPolicy::AllowList,
25 }
26 }
27
28 #[must_use]
30 pub fn seed_list(self) -> &'static [IdentityHash] {
31 match self {
32 RequestEndpointPolicy::AllowList(list) => list,
33 _ => &[],
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Decline {
40 Ignore,
44 CloseLink,
45 ResponseTooLarge,
46}
47
48pub trait ResponseSink {
49 fn put_packed(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded>;
50
51 fn put_bytes(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded>;
52
53 fn put_static_bytes(&mut self, bytes: &'static [u8]) -> Result<(), ResponseCapacityExceeded> {
54 self.put_bytes(bytes)
55 }
56
57 fn put_static_file(
58 &mut self,
59 _name: &'static str,
60 _bytes: &'static [u8],
61 ) -> Result<(), ResponseCapacityExceeded> {
62 Err(ResponseCapacityExceeded)
63 }
64
65 #[cfg(feature = "std")]
66 fn put_open_bytes(
67 &mut self,
68 _file: std::fs::File,
69 _byte_len: u64,
70 ) -> Result<(), ResponseCapacityExceeded> {
71 Err(ResponseCapacityExceeded)
72 }
73
74 #[cfg(feature = "std")]
75 fn put_open_file(
76 &mut self,
77 _name: &str,
78 _file: std::fs::File,
79 _byte_len: u64,
80 ) -> Result<(), ResponseCapacityExceeded> {
81 Err(ResponseCapacityExceeded)
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct ResponseCapacityExceeded;
87
88#[cfg(feature = "alloc")]
89impl ResponseSink for alloc::vec::Vec<u8> {
90 fn put_packed(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
91 self.extend_from_slice(bytes);
92 Ok(())
93 }
94
95 fn put_bytes(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
96 let mut header = [0u8; MAX_PACKED_BINARY_HEADER_LEN];
97 let header_len = write_packed_binary_header(bytes.len(), &mut header)
98 .map_err(|_| ResponseCapacityExceeded)?;
99 self.reserve(header_len + bytes.len());
100 self.extend_from_slice(&header[..header_len]);
101 self.extend_from_slice(bytes);
102 Ok(())
103 }
104}
105
106impl<const N: usize> ResponseSink for heapless::Vec<u8, N> {
107 fn put_packed(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
108 self.extend_from_slice(bytes)
109 .map_err(|_| ResponseCapacityExceeded)
110 }
111
112 fn put_bytes(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
113 let packed_len = packed_binary_len(bytes.len()).ok_or(ResponseCapacityExceeded)?;
114 if self.capacity() - self.len() < packed_len {
115 return Err(ResponseCapacityExceeded);
116 }
117 let mut header = [0u8; MAX_PACKED_BINARY_HEADER_LEN];
118 let header_len = write_packed_binary_header(bytes.len(), &mut header)
119 .map_err(|_| ResponseCapacityExceeded)?;
120 self.extend_from_slice(&header[..header_len])
121 .map_err(|_| ResponseCapacityExceeded)?;
122 self.extend_from_slice(bytes)
123 .map_err(|_| ResponseCapacityExceeded)
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct RespondToken {
131 pub link_id: LinkId,
132 pub request_id: RequestId,
133 pub rtt: RttMillis,
135}
136
137pub struct InboundRequest<'a> {
138 pub destination: DestinationHash,
139 pub data: &'a [u8],
140 pub requester: Option<IdentityHash>,
141 pub requested_at: InstantMillis,
142 respond_token: RespondToken,
143}
144
145impl<'a> InboundRequest<'a> {
146 #[must_use]
147 pub fn new(
148 destination: DestinationHash,
149 link_id: LinkId,
150 request_id: RequestId,
151 requester: Option<IdentityHash>,
152 requested_at: InstantMillis,
153 rtt: RttMillis,
154 data: &'a [u8],
155 ) -> Self {
156 Self {
157 destination,
158 data,
159 requester,
160 requested_at,
161 respond_token: RespondToken {
162 link_id,
163 request_id,
164 rtt,
165 },
166 }
167 }
168
169 #[must_use]
170 pub fn respond_token(&self) -> RespondToken {
171 self.respond_token
172 }
173}
174
175pub struct RequestContext<'a, S> {
176 pub state: &'a S,
177 pub destination: DestinationHash,
178 pub data: &'a [u8],
179 pub requester: Option<IdentityHash>,
180 pub requested_at: InstantMillis,
181 respond_token: RespondToken,
182 sink: &'a mut dyn ResponseSink,
183}
184
185impl<S> RequestContext<'_, S> {
186 pub fn respond(&mut self, data: impl AsRef<[u8]>) -> Result<(), Decline> {
199 self.sink
200 .put_packed(data.as_ref())
201 .map_err(|_| Decline::ResponseTooLarge)
202 }
203
204 #[deprecated(note = "use RequestContext::respond for exact application payloads")]
206 #[doc(hidden)]
207 pub fn respond_packed(&mut self, bytes: &[u8]) -> Result<(), Decline> {
208 self.respond(bytes)
209 }
210
211 pub fn respond_messagepack_bytes(&mut self, bytes: &[u8]) -> Result<(), Decline> {
213 self.sink
214 .put_bytes(bytes)
215 .map_err(|_| Decline::ResponseTooLarge)
216 }
217
218 #[deprecated(
221 note = "use RequestContext::respond_messagepack_bytes for a MessagePack bin value"
222 )]
223 #[doc(hidden)]
224 pub fn respond_bytes(&mut self, bytes: &[u8]) -> Result<(), Decline> {
225 self.respond_messagepack_bytes(bytes)
226 }
227
228 pub fn respond_static_messagepack_bytes(
230 &mut self,
231 bytes: &'static [u8],
232 ) -> Result<(), Decline> {
233 self.sink
234 .put_static_bytes(bytes)
235 .map_err(|_| Decline::ResponseTooLarge)
236 }
237
238 #[deprecated(
241 note = "use RequestContext::respond_static_messagepack_bytes for a static MessagePack bin value"
242 )]
243 #[doc(hidden)]
244 pub fn respond_static_bytes(&mut self, bytes: &'static [u8]) -> Result<(), Decline> {
245 self.respond_static_messagepack_bytes(bytes)
246 }
247
248 pub fn respond_static_file(
253 &mut self,
254 name: &'static str,
255 bytes: &'static [u8],
256 ) -> Result<(), Decline> {
257 self.sink
258 .put_static_file(name, bytes)
259 .map_err(|_| Decline::ResponseTooLarge)
260 }
261
262 #[cfg(feature = "std")]
268 #[doc(hidden)]
269 pub fn respond_open_bytes(
270 &mut self,
271 file: std::fs::File,
272 byte_len: u64,
273 ) -> Result<(), Decline> {
274 self.sink
275 .put_open_bytes(file, byte_len)
276 .map_err(|_| Decline::ResponseTooLarge)
277 }
278
279 #[cfg(feature = "std")]
284 #[doc(hidden)]
285 pub fn respond_open_file(
286 &mut self,
287 name: &str,
288 file: std::fs::File,
289 byte_len: u64,
290 ) -> Result<(), Decline> {
291 self.sink
292 .put_open_file(name, file, byte_len)
293 .map_err(|_| Decline::ResponseTooLarge)
294 }
295
296 pub fn write_packed(&mut self, bytes: &[u8]) -> Result<&mut Self, ResponseCapacityExceeded> {
297 self.sink.put_packed(bytes)?;
298 Ok(self)
299 }
300
301 #[must_use]
305 pub fn respond_token(&self) -> RespondToken {
306 self.respond_token
307 }
308}
309
310pub type RequestEndpointId = RequestPathHash;
312
313#[allow(async_fn_in_trait)]
314pub trait RequestEndpoint<AppState = ()> {
315 const ENDPOINT_ID: &'static str;
317 const POLICY: RequestEndpointPolicy;
318 async fn handle(context: RequestContext<'_, AppState>) -> Result<(), Decline>;
319}
320
321#[allow(async_fn_in_trait)]
324pub trait RequestEndpointSet<S> {
325 const REGISTRATIONS: &'static [(&'static str, RequestEndpointPolicy)];
326 async fn dispatch(cx: RequestContext<'_, S>, path_hash: RequestPathHash)
327 -> Result<(), Decline>;
328}
329
330impl<S> RequestEndpointSet<S> for () {
333 const REGISTRATIONS: &'static [(&'static str, RequestEndpointPolicy)] = &[];
334 async fn dispatch(
335 _cx: RequestContext<'_, S>,
336 _path_hash: RequestPathHash,
337 ) -> Result<(), Decline> {
338 Err(Decline::Ignore)
339 }
340}
341
342pub const fn no_request_endpoints() {}
346
347pub async fn dispatch_request<'a, S, R: RequestEndpointSet<S>>(
351 state: &'a S,
352 path_hash: RequestPathHash,
353 request: InboundRequest<'a>,
354 sink: &'a mut dyn ResponseSink,
355) -> Result<(), Decline> {
356 let cx = RequestContext {
357 state,
358 destination: request.destination,
359 data: request.data,
360 requester: request.requester,
361 requested_at: request.requested_at,
362 respond_token: request.respond_token(),
363 sink,
364 };
365 R::dispatch(cx, path_hash).await
366}
367
368#[macro_export]
371macro_rules! request_endpoints {
372 () => {
373 $crate::runtime::request_endpoints::no_request_endpoints()
374 };
375 ($($endpoint:ty),+ $(,)?) => {{
376 struct RequestEndpointSetImpl;
377 impl<S> $crate::runtime::request_endpoints::RequestEndpointSet<S> for RequestEndpointSetImpl
378 where
379 $($endpoint: $crate::runtime::request_endpoints::RequestEndpoint<S>,)+
380 {
381 const REGISTRATIONS: &'static [(&'static str, $crate::runtime::request_endpoints::RequestEndpointPolicy)] = &[
382 $((
383 <$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::ENDPOINT_ID,
384 <$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::POLICY,
385 ),)+
386 ];
387
388 async fn dispatch(
389 cx: $crate::runtime::request_endpoints::RequestContext<'_, S>,
390 path_hash: $crate::routing::request_handlers::RequestPathHash,
391 ) -> ::core::result::Result<(), $crate::runtime::request_endpoints::Decline> {
392 $(
393 if path_hash
394 == $crate::routing::request_handlers::RequestPathHash::of(
395 <$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::ENDPOINT_ID,
396 )
397 {
398 return <$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::handle(cx).await;
399 }
400 )+
401 ::core::result::Result::Err($crate::runtime::request_endpoints::Decline::Ignore)
402 }
403 }
404 RequestEndpointSetImpl
405 }};
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 struct App {
413 greeting: &'static [u8],
414 }
415
416 struct Health;
417 impl RequestEndpoint<App> for Health {
418 const ENDPOINT_ID: &'static str = "/health";
419 const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowAll;
420 async fn handle(mut cx: RequestContext<'_, App>) -> Result<(), Decline> {
421 cx.respond("ok")
422 }
423 }
424
425 struct Greet;
426 impl RequestEndpoint<App> for Greet {
427 const ENDPOINT_ID: &'static str = "/greet";
428 const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowAll;
429 async fn handle(mut cx: RequestContext<'_, App>) -> Result<(), Decline> {
430 let greeting = cx.state.greeting;
431 cx.respond(greeting)
432 }
433 }
434
435 const ADMIN: IdentityHash = IdentityHash::new([0xAD; 16]);
436
437 struct Admin;
438 impl RequestEndpoint<App> for Admin {
439 const ENDPOINT_ID: &'static str = "/admin";
440 const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowList(&[ADMIN]);
441 async fn handle(_cx: RequestContext<'_, App>) -> Result<(), Decline> {
442 Err(Decline::CloseLink)
443 }
444 }
445
446 struct Ack;
447 impl RequestEndpoint<App> for Ack {
448 const ENDPOINT_ID: &'static str = "/ack";
449 const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowAll;
450 async fn handle(mut cx: RequestContext<'_, App>) -> Result<(), Decline> {
451 cx.respond([0u8; 0])
452 }
453 }
454
455 fn registrations<R: RequestEndpointSet<App>>(
456 _endpoints: R,
457 ) -> &'static [(&'static str, RequestEndpointPolicy)] {
458 R::REGISTRATIONS
459 }
460
461 #[test]
462 fn the_endpoint_set_is_the_registration_set_the_recipe_stands_up() {
463 let registrations = registrations(crate::request_endpoints![Health, Greet, Admin, Ack]);
464 assert_eq!(registrations.len(), 4);
465 assert_eq!(
466 registrations[0],
467 ("/health", RequestEndpointPolicy::AllowAll)
468 );
469 assert_eq!(registrations[2].0, "/admin");
470 assert_eq!(registrations[2].1.engine_policy(), RequestPolicy::AllowList);
471 assert_eq!(registrations[2].1.seed_list(), &[ADMIN]);
472 assert_eq!(registrations[0].1.engine_policy(), RequestPolicy::AllowAll);
473 assert!(registrations[0].1.seed_list().is_empty());
474 }
475
476 #[test]
477 fn messagepack_binary_sinks_frame_atomically() {
478 let mut exact = heapless::Vec::<u8, 7>::new();
479 exact.put_bytes(b"hello").unwrap();
480 assert_eq!(exact.as_slice(), &[0xC4, 5, b'h', b'e', b'l', b'l', b'o']);
481
482 let mut short = heapless::Vec::<u8, 6>::new();
483 assert_eq!(short.put_bytes(b"hello"), Err(ResponseCapacityExceeded));
484 assert!(short.is_empty());
485 }
486
487 #[cfg(feature = "alloc")]
488 #[test]
489 fn dispatch_endpoints_by_path_then_answers_or_declines() {
490 futures_executor::block_on(async {
491 async fn dispatch<R: RequestEndpointSet<App>>(
492 _endpoints: &R,
493 state: &App,
494 path: &str,
495 sink: &mut dyn ResponseSink,
496 ) -> Result<(), Decline> {
497 let request = InboundRequest::new(
498 DestinationHash::new([3; 16]),
499 LinkId::new([1; 16]),
500 RequestId([2; 16]),
501 None,
502 InstantMillis(0),
503 RttMillis::new(0),
504 b"",
505 );
506 dispatch_request::<App, R>(state, RequestPathHash::of(path), request, sink).await
507 }
508
509 let endpoints = crate::request_endpoints![Health, Greet, Admin, Ack];
510 let state = App { greeting: b"hi" };
511
512 let mut greet = std::vec::Vec::new();
513 assert_eq!(
514 dispatch(&endpoints, &state, "/greet", &mut greet).await,
515 Ok(())
516 );
517 assert_eq!(greet.as_slice(), b"hi");
518
519 let mut health = std::vec::Vec::new();
520 assert_eq!(
521 dispatch(&endpoints, &state, "/health", &mut health).await,
522 Ok(())
523 );
524 assert_eq!(health.as_slice(), b"ok");
525
526 let mut ack = std::vec::Vec::new();
527 assert_eq!(dispatch(&endpoints, &state, "/ack", &mut ack).await, Ok(()));
528 assert!(ack.is_empty());
529
530 let mut admin = std::vec::Vec::new();
531 assert_eq!(
532 dispatch(&endpoints, &state, "/admin", &mut admin).await,
533 Err(Decline::CloseLink)
534 );
535
536 let mut miss = std::vec::Vec::new();
537 assert_eq!(
538 dispatch(&endpoints, &state, "/nope", &mut miss).await,
539 Err(Decline::Ignore)
540 );
541 });
542 }
543}