Skip to main content

topcoat_runtime/
procedure.rs

1use std::{hash::Hash, pin::Pin};
2
3use serde::{Deserialize, Serialize};
4use topcoat_core::{context::Cx, error::Result};
5use topcoat_router::{
6    Body, Method, Methods, Path, PathBuf, Route, RouteFuture, RouteId, RouterBuilder,
7    response::Response,
8};
9
10use crate::{Surrogate, Surrogated};
11
12const PROCEDURE_ROUTE_PREFIX: &str = "/_topcoat/procedures";
13
14/// The identity of a procedure, stable across the server and the client
15/// runtime.
16#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(transparent)]
18pub struct ProcedureId(&'static str);
19
20impl ProcedureId {
21    #[must_use]
22    pub const fn new(inner: &'static str) -> Self {
23        Self(inner)
24    }
25
26    #[must_use]
27    fn as_str(&self) -> &str {
28        self.0
29    }
30}
31
32/// The future returned by [`Procedure::handle`]: a boxed, `Send` future
33/// borrowing the procedure and its request context.
34pub type ProcedureFuture<'cx> = Pin<Box<dyn Future<Output = Result<Response>> + Send + 'cx>>;
35
36/// An async server function callable from the client runtime.
37///
38/// Registered into a [`RouterBuilder`] with
39/// [`procedure`](RouterBuilderProcedureExt::procedure), which serves it as a
40/// route dispatched by [`ProcedureId`].
41pub trait Procedure: Send + Sync + 'static {
42    /// The identity of this procedure.
43    fn id(&self) -> ProcedureId;
44
45    /// Handles a procedure call, deserializing its arguments from `body`.
46    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> ProcedureFuture<'cx>;
47}
48
49impl<P: Procedure + ?Sized> Procedure for &'static P {
50    fn id(&self) -> ProcedureId {
51        (**self).id()
52    }
53
54    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> ProcedureFuture<'cx> {
55        (**self).handle(cx, body)
56    }
57}
58
59#[cfg(feature = "discover")]
60inventory::collect!(&'static dyn Procedure);
61
62/// The argument and return types of a [`Procedure`], as seen by runtime
63/// expressions calling it.
64pub trait TypedProcedure: Procedure {
65    /// The arguments, as a tuple in declaration order.
66    type Args: Surrogated;
67
68    /// The value a successful call resolves to.
69    type Output: Surrogated;
70}
71
72/// A [`Route`] that handles calls to one server procedure.
73pub struct ProcedureRoute {
74    id: RouteId,
75    path: PathBuf,
76    procedure: Box<dyn Procedure>,
77}
78
79impl ProcedureRoute {
80    /// Builds the route that serves `procedure`.
81    pub fn new(procedure: impl Procedure) -> Self {
82        Self {
83            id: RouteId::new(),
84            path: Path::new(&format!(
85                "{PROCEDURE_ROUTE_PREFIX}/{}",
86                procedure.id().as_str()
87            ))
88            .to_owned(),
89            procedure: Box::new(procedure),
90        }
91    }
92}
93
94impl Route for ProcedureRoute {
95    fn id(&self) -> RouteId {
96        self.id
97    }
98
99    fn methods(&self) -> Methods<'_> {
100        Methods::Only(&[Method::POST])
101    }
102
103    fn path(&self) -> &Path {
104        &self.path
105    }
106
107    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
108        self.procedure.handle(cx, body)
109    }
110}
111
112/// Registers server procedures on a [`RouterBuilder`].
113pub trait RouterBuilderProcedureExt {
114    /// Mounts a procedure route.
115    #[must_use]
116    fn procedure(self, procedure: impl Procedure) -> Self;
117
118    /// Registers every procedure linked into the binary.
119    #[cfg(feature = "discover")]
120    #[must_use]
121    fn discover_procedures(self) -> Self;
122}
123
124impl RouterBuilderProcedureExt for RouterBuilder {
125    fn procedure(self, procedure: impl Procedure) -> Self {
126        self.route(ProcedureRoute::new(procedure))
127    }
128
129    #[cfg(feature = "discover")]
130    fn discover_procedures(mut self) -> Self {
131        for &procedure in inventory::iter::<&'static dyn Procedure>() {
132            self = self.procedure(procedure);
133        }
134        self
135    }
136}
137
138/// The surrogate a [`Procedure`] value turns into inside a runtime
139/// expression.
140///
141/// Captured as a `&'static` reference, so closures inside the expression can
142/// hold it without borrowing a local. Serializes as the procedure's id, so
143/// the browser can call it back, and exposes the typed [`call`](Self::call)
144/// that runtime expressions invoke.
145pub struct ProcedureSurrogate<P>(P);
146
147impl<P: TypedProcedure> ProcedureSurrogate<P> {
148    #[must_use]
149    pub const fn new(procedure: P) -> Self {
150        Self(procedure)
151    }
152
153    /// Invokes the procedure from the client side.
154    ///
155    /// # Panics
156    ///
157    /// Always panics; procedures can only be invoked from the client runtime.
158    #[allow(clippy::unused_async)]
159    pub async fn call(
160        &self,
161        _args: <P::Args as Surrogated>::Surrogate,
162    ) -> <P::Output as Surrogated>::Surrogate {
163        panic!("procedures cannot be executed on the server");
164    }
165}
166
167impl<P> Surrogate for &'static ProcedureSurrogate<P>
168where
169    P: TypedProcedure + Copy + Surrogated<Surrogate = Self>,
170{
171    type Real = P;
172
173    fn into_real(self) -> Self::Real {
174        self.0
175    }
176}
177
178impl<P: TypedProcedure> Serialize for ProcedureSurrogate<P> {
179    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
180    where
181        S: serde::Serializer,
182    {
183        #[derive(Serialize)]
184        struct TaggedProcedure {
185            t: &'static str,
186            id: ProcedureId,
187        }
188
189        TaggedProcedure {
190            t: "Procedure",
191            id: self.0.id(),
192        }
193        .serialize(serializer)
194    }
195}