use crate::engine::InstantMillis;
use crate::identity::IdentityHash;
use crate::routing::links::request::{
packed_binary_len, write_packed_binary_header, RequestId, MAX_PACKED_BINARY_HEADER_LEN,
};
use crate::routing::links::LinkId;
use crate::routing::request_handlers::{RequestPathHash, RequestPolicy};
use crate::units::RttMillis;
use crate::wire::DestinationHash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestEndpointPolicy {
AllowNone,
AllowAll,
AllowList(&'static [IdentityHash]),
}
impl RequestEndpointPolicy {
#[must_use]
pub fn engine_policy(self) -> RequestPolicy {
match self {
RequestEndpointPolicy::AllowNone => RequestPolicy::AllowNone,
RequestEndpointPolicy::AllowAll => RequestPolicy::AllowAll,
RequestEndpointPolicy::AllowList(_) => RequestPolicy::AllowList,
}
}
#[must_use]
pub fn seed_list(self) -> &'static [IdentityHash] {
match self {
RequestEndpointPolicy::AllowList(list) => list,
_ => &[],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decline {
Ignore,
CloseLink,
ResponseTooLarge,
}
pub trait ResponseSink {
fn put_packed(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded>;
fn put_bytes(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded>;
fn put_static_bytes(&mut self, bytes: &'static [u8]) -> Result<(), ResponseCapacityExceeded> {
self.put_bytes(bytes)
}
fn put_static_file(
&mut self,
_name: &'static str,
_bytes: &'static [u8],
) -> Result<(), ResponseCapacityExceeded> {
Err(ResponseCapacityExceeded)
}
#[cfg(feature = "std")]
fn put_open_bytes(
&mut self,
_file: std::fs::File,
_byte_len: u64,
) -> Result<(), ResponseCapacityExceeded> {
Err(ResponseCapacityExceeded)
}
#[cfg(feature = "std")]
fn put_open_file(
&mut self,
_name: &str,
_file: std::fs::File,
_byte_len: u64,
) -> Result<(), ResponseCapacityExceeded> {
Err(ResponseCapacityExceeded)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponseCapacityExceeded;
#[cfg(feature = "alloc")]
impl ResponseSink for alloc::vec::Vec<u8> {
fn put_packed(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
self.extend_from_slice(bytes);
Ok(())
}
fn put_bytes(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
let mut header = [0u8; MAX_PACKED_BINARY_HEADER_LEN];
let header_len = write_packed_binary_header(bytes.len(), &mut header)
.map_err(|_| ResponseCapacityExceeded)?;
self.reserve(header_len + bytes.len());
self.extend_from_slice(&header[..header_len]);
self.extend_from_slice(bytes);
Ok(())
}
}
impl<const N: usize> ResponseSink for heapless::Vec<u8, N> {
fn put_packed(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
self.extend_from_slice(bytes)
.map_err(|_| ResponseCapacityExceeded)
}
fn put_bytes(&mut self, bytes: &[u8]) -> Result<(), ResponseCapacityExceeded> {
let packed_len = packed_binary_len(bytes.len()).ok_or(ResponseCapacityExceeded)?;
if self.capacity() - self.len() < packed_len {
return Err(ResponseCapacityExceeded);
}
let mut header = [0u8; MAX_PACKED_BINARY_HEADER_LEN];
let header_len = write_packed_binary_header(bytes.len(), &mut header)
.map_err(|_| ResponseCapacityExceeded)?;
self.extend_from_slice(&header[..header_len])
.map_err(|_| ResponseCapacityExceeded)?;
self.extend_from_slice(bytes)
.map_err(|_| ResponseCapacityExceeded)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RespondToken {
pub link_id: LinkId,
pub request_id: RequestId,
pub rtt: RttMillis,
}
pub struct InboundRequest<'a> {
pub destination: DestinationHash,
pub data: &'a [u8],
pub requester: Option<IdentityHash>,
pub requested_at: InstantMillis,
respond_token: RespondToken,
}
impl<'a> InboundRequest<'a> {
#[must_use]
pub fn new(
destination: DestinationHash,
link_id: LinkId,
request_id: RequestId,
requester: Option<IdentityHash>,
requested_at: InstantMillis,
rtt: RttMillis,
data: &'a [u8],
) -> Self {
Self {
destination,
data,
requester,
requested_at,
respond_token: RespondToken {
link_id,
request_id,
rtt,
},
}
}
#[must_use]
pub fn respond_token(&self) -> RespondToken {
self.respond_token
}
}
pub struct RequestContext<'a, S> {
pub state: &'a S,
pub destination: DestinationHash,
pub data: &'a [u8],
pub requester: Option<IdentityHash>,
pub requested_at: InstantMillis,
respond_token: RespondToken,
sink: &'a mut dyn ResponseSink,
}
impl<S> RequestContext<'_, S> {
pub fn respond(&mut self, data: impl AsRef<[u8]>) -> Result<(), Decline> {
self.sink
.put_packed(data.as_ref())
.map_err(|_| Decline::ResponseTooLarge)
}
#[deprecated(note = "use RequestContext::respond for exact application payloads")]
#[doc(hidden)]
pub fn respond_packed(&mut self, bytes: &[u8]) -> Result<(), Decline> {
self.respond(bytes)
}
pub fn respond_messagepack_bytes(&mut self, bytes: &[u8]) -> Result<(), Decline> {
self.sink
.put_bytes(bytes)
.map_err(|_| Decline::ResponseTooLarge)
}
#[deprecated(
note = "use RequestContext::respond_messagepack_bytes for a MessagePack bin value"
)]
#[doc(hidden)]
pub fn respond_bytes(&mut self, bytes: &[u8]) -> Result<(), Decline> {
self.respond_messagepack_bytes(bytes)
}
pub fn respond_static_messagepack_bytes(
&mut self,
bytes: &'static [u8],
) -> Result<(), Decline> {
self.sink
.put_static_bytes(bytes)
.map_err(|_| Decline::ResponseTooLarge)
}
#[deprecated(
note = "use RequestContext::respond_static_messagepack_bytes for a static MessagePack bin value"
)]
#[doc(hidden)]
pub fn respond_static_bytes(&mut self, bytes: &'static [u8]) -> Result<(), Decline> {
self.respond_static_messagepack_bytes(bytes)
}
pub fn respond_static_file(
&mut self,
name: &'static str,
bytes: &'static [u8],
) -> Result<(), Decline> {
self.sink
.put_static_file(name, bytes)
.map_err(|_| Decline::ResponseTooLarge)
}
#[cfg(feature = "std")]
#[doc(hidden)]
pub fn respond_open_bytes(
&mut self,
file: std::fs::File,
byte_len: u64,
) -> Result<(), Decline> {
self.sink
.put_open_bytes(file, byte_len)
.map_err(|_| Decline::ResponseTooLarge)
}
#[cfg(feature = "std")]
#[doc(hidden)]
pub fn respond_open_file(
&mut self,
name: &str,
file: std::fs::File,
byte_len: u64,
) -> Result<(), Decline> {
self.sink
.put_open_file(name, file, byte_len)
.map_err(|_| Decline::ResponseTooLarge)
}
pub fn write_packed(&mut self, bytes: &[u8]) -> Result<&mut Self, ResponseCapacityExceeded> {
self.sink.put_packed(bytes)?;
Ok(self)
}
#[must_use]
pub fn respond_token(&self) -> RespondToken {
self.respond_token
}
}
pub type RequestEndpointId = RequestPathHash;
#[allow(async_fn_in_trait)]
pub trait RequestEndpoint<AppState = ()> {
const ENDPOINT_ID: &'static str;
const POLICY: RequestEndpointPolicy;
async fn handle(context: RequestContext<'_, AppState>) -> Result<(), Decline>;
}
#[allow(async_fn_in_trait)]
pub trait RequestEndpointSet<S> {
const REGISTRATIONS: &'static [(&'static str, RequestEndpointPolicy)];
async fn dispatch(cx: RequestContext<'_, S>, path_hash: RequestPathHash)
-> Result<(), Decline>;
}
impl<S> RequestEndpointSet<S> for () {
const REGISTRATIONS: &'static [(&'static str, RequestEndpointPolicy)] = &[];
async fn dispatch(
_cx: RequestContext<'_, S>,
_path_hash: RequestPathHash,
) -> Result<(), Decline> {
Err(Decline::Ignore)
}
}
pub const fn no_request_endpoints() {}
pub async fn dispatch_request<'a, S, R: RequestEndpointSet<S>>(
state: &'a S,
path_hash: RequestPathHash,
request: InboundRequest<'a>,
sink: &'a mut dyn ResponseSink,
) -> Result<(), Decline> {
let cx = RequestContext {
state,
destination: request.destination,
data: request.data,
requester: request.requester,
requested_at: request.requested_at,
respond_token: request.respond_token(),
sink,
};
R::dispatch(cx, path_hash).await
}
#[macro_export]
macro_rules! request_endpoints {
() => {
$crate::runtime::request_endpoints::no_request_endpoints()
};
($($endpoint:ty),+ $(,)?) => {{
struct RequestEndpointSetImpl;
impl<S> $crate::runtime::request_endpoints::RequestEndpointSet<S> for RequestEndpointSetImpl
where
$($endpoint: $crate::runtime::request_endpoints::RequestEndpoint<S>,)+
{
const REGISTRATIONS: &'static [(&'static str, $crate::runtime::request_endpoints::RequestEndpointPolicy)] = &[
$((
<$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::ENDPOINT_ID,
<$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::POLICY,
),)+
];
async fn dispatch(
cx: $crate::runtime::request_endpoints::RequestContext<'_, S>,
path_hash: $crate::routing::request_handlers::RequestPathHash,
) -> ::core::result::Result<(), $crate::runtime::request_endpoints::Decline> {
$(
if path_hash
== $crate::routing::request_handlers::RequestPathHash::of(
<$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::ENDPOINT_ID,
)
{
return <$endpoint as $crate::runtime::request_endpoints::RequestEndpoint<S>>::handle(cx).await;
}
)+
::core::result::Result::Err($crate::runtime::request_endpoints::Decline::Ignore)
}
}
RequestEndpointSetImpl
}};
}
#[cfg(test)]
mod tests {
use super::*;
struct App {
greeting: &'static [u8],
}
struct Health;
impl RequestEndpoint<App> for Health {
const ENDPOINT_ID: &'static str = "/health";
const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowAll;
async fn handle(mut cx: RequestContext<'_, App>) -> Result<(), Decline> {
cx.respond("ok")
}
}
struct Greet;
impl RequestEndpoint<App> for Greet {
const ENDPOINT_ID: &'static str = "/greet";
const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowAll;
async fn handle(mut cx: RequestContext<'_, App>) -> Result<(), Decline> {
let greeting = cx.state.greeting;
cx.respond(greeting)
}
}
const ADMIN: IdentityHash = IdentityHash::new([0xAD; 16]);
struct Admin;
impl RequestEndpoint<App> for Admin {
const ENDPOINT_ID: &'static str = "/admin";
const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowList(&[ADMIN]);
async fn handle(_cx: RequestContext<'_, App>) -> Result<(), Decline> {
Err(Decline::CloseLink)
}
}
struct Ack;
impl RequestEndpoint<App> for Ack {
const ENDPOINT_ID: &'static str = "/ack";
const POLICY: RequestEndpointPolicy = RequestEndpointPolicy::AllowAll;
async fn handle(mut cx: RequestContext<'_, App>) -> Result<(), Decline> {
cx.respond([0u8; 0])
}
}
fn registrations<R: RequestEndpointSet<App>>(
_endpoints: R,
) -> &'static [(&'static str, RequestEndpointPolicy)] {
R::REGISTRATIONS
}
#[test]
fn the_endpoint_set_is_the_registration_set_the_recipe_stands_up() {
let registrations = registrations(crate::request_endpoints![Health, Greet, Admin, Ack]);
assert_eq!(registrations.len(), 4);
assert_eq!(
registrations[0],
("/health", RequestEndpointPolicy::AllowAll)
);
assert_eq!(registrations[2].0, "/admin");
assert_eq!(registrations[2].1.engine_policy(), RequestPolicy::AllowList);
assert_eq!(registrations[2].1.seed_list(), &[ADMIN]);
assert_eq!(registrations[0].1.engine_policy(), RequestPolicy::AllowAll);
assert!(registrations[0].1.seed_list().is_empty());
}
#[test]
fn messagepack_binary_sinks_frame_atomically() {
let mut exact = heapless::Vec::<u8, 7>::new();
exact.put_bytes(b"hello").unwrap();
assert_eq!(exact.as_slice(), &[0xC4, 5, b'h', b'e', b'l', b'l', b'o']);
let mut short = heapless::Vec::<u8, 6>::new();
assert_eq!(short.put_bytes(b"hello"), Err(ResponseCapacityExceeded));
assert!(short.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn dispatch_endpoints_by_path_then_answers_or_declines() {
futures_executor::block_on(async {
async fn dispatch<R: RequestEndpointSet<App>>(
_endpoints: &R,
state: &App,
path: &str,
sink: &mut dyn ResponseSink,
) -> Result<(), Decline> {
let request = InboundRequest::new(
DestinationHash::new([3; 16]),
LinkId::new([1; 16]),
RequestId([2; 16]),
None,
InstantMillis(0),
RttMillis::new(0),
b"",
);
dispatch_request::<App, R>(state, RequestPathHash::of(path), request, sink).await
}
let endpoints = crate::request_endpoints![Health, Greet, Admin, Ack];
let state = App { greeting: b"hi" };
let mut greet = std::vec::Vec::new();
assert_eq!(
dispatch(&endpoints, &state, "/greet", &mut greet).await,
Ok(())
);
assert_eq!(greet.as_slice(), b"hi");
let mut health = std::vec::Vec::new();
assert_eq!(
dispatch(&endpoints, &state, "/health", &mut health).await,
Ok(())
);
assert_eq!(health.as_slice(), b"ok");
let mut ack = std::vec::Vec::new();
assert_eq!(dispatch(&endpoints, &state, "/ack", &mut ack).await, Ok(()));
assert!(ack.is_empty());
let mut admin = std::vec::Vec::new();
assert_eq!(
dispatch(&endpoints, &state, "/admin", &mut admin).await,
Err(Decline::CloseLink)
);
let mut miss = std::vec::Vec::new();
assert_eq!(
dispatch(&endpoints, &state, "/nope", &mut miss).await,
Err(Decline::Ignore)
);
});
}
}