Skip to main content

topcoat_runtime/
procedure.rs

1use std::{hash::Hash, marker::PhantomData, pin::Pin};
2
3use ref_cast::RefCast;
4use serde::{Deserialize, Serialize};
5use topcoat_core::{context::Cx, error::Result};
6use topcoat_router::{Body, Method, Path, PathBuf, Response, Route, RouteFuture, RouterBuilder};
7
8use crate::Surrogated;
9
10const PROCEDURE_ROUTE_PREFIX: &str = "/_topcoat/procedures";
11
12#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct ProcedureId(&'static str);
15
16impl ProcedureId {
17    #[must_use]
18    pub const fn new(inner: &'static str) -> Self {
19        Self(inner)
20    }
21
22    #[must_use]
23    fn as_str(&self) -> &str {
24        self.0
25    }
26}
27
28pub type ProcedureHandlerFn =
29    for<'cx> fn(
30        cx: &'cx Cx,
31        body: Body,
32    ) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'cx>>;
33
34#[derive(Debug, Clone)]
35pub struct Procedure<A, R> {
36    id: ProcedureId,
37    handle: ProcedureHandlerFn,
38    _phantom: PhantomData<fn(A) -> R>,
39}
40
41impl<A, R> Procedure<A, R> {
42    #[inline]
43    pub const fn new(id: ProcedureId, handle: ProcedureHandlerFn) -> Self {
44        Self {
45            id,
46            handle,
47            _phantom: PhantomData,
48        }
49    }
50
51    #[must_use]
52    pub fn id(&self) -> ProcedureId {
53        self.id
54    }
55}
56
57#[derive(Debug, Clone)]
58pub struct ErasedProcedure {
59    id: ProcedureId,
60    handle: ProcedureHandlerFn,
61}
62
63impl ErasedProcedure {
64    #[must_use]
65    pub const fn new<A, R>(procedure: &Procedure<A, R>) -> Self {
66        Self {
67            id: procedure.id,
68            handle: procedure.handle,
69        }
70    }
71
72    #[must_use]
73    pub fn id(&self) -> ProcedureId {
74        self.id
75    }
76
77    /// Dispatches the procedure call, awaiting its handler future.
78    ///
79    /// # Errors
80    ///
81    /// Propagates any error returned by the underlying procedure handler.
82    #[inline]
83    pub async fn handle(&self, cx: &Cx, body: Body) -> Result<Response> {
84        (self.handle)(cx, body).await
85    }
86}
87
88impl<A, R> From<Procedure<A, R>> for ErasedProcedure {
89    fn from(value: Procedure<A, R>) -> Self {
90        Self {
91            id: value.id,
92            handle: value.handle,
93        }
94    }
95}
96
97impl<A, R> From<&Procedure<A, R>> for ErasedProcedure {
98    fn from(value: &Procedure<A, R>) -> Self {
99        Self::new(value)
100    }
101}
102
103#[cfg(feature = "discover")]
104inventory::collect!(ErasedProcedure);
105
106/// A [`Route`] that handles calls to one server procedure.
107#[derive(Debug, Clone)]
108pub struct ProcedureRoute {
109    path: PathBuf,
110    procedure: ErasedProcedure,
111}
112
113impl ProcedureRoute {
114    /// Builds the route that serves `procedure`.
115    pub fn new(procedure: impl Into<ErasedProcedure>) -> Self {
116        let procedure = procedure.into();
117        Self {
118            path: Path::new(&format!(
119                "{PROCEDURE_ROUTE_PREFIX}/{}",
120                procedure.id().as_str()
121            ))
122            .to_owned(),
123            procedure,
124        }
125    }
126}
127
128impl Route for ProcedureRoute {
129    fn method(&self) -> Method {
130        Method::POST
131    }
132
133    fn path(&self) -> &Path {
134        &self.path
135    }
136
137    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
138        Box::pin(async move { self.procedure.handle(cx, body).await })
139    }
140}
141
142/// Registers server procedures on a [`RouterBuilder`].
143pub trait RouterBuilderProcedureExt {
144    /// Mounts a procedure route.
145    #[must_use]
146    fn procedure(self, procedure: impl Into<ErasedProcedure>) -> Self;
147
148    /// Registers every procedure linked into the binary.
149    #[cfg(feature = "discover")]
150    #[must_use]
151    fn discover_procedures(self) -> Self;
152}
153
154impl RouterBuilderProcedureExt for RouterBuilder {
155    fn procedure(self, procedure: impl Into<ErasedProcedure>) -> Self {
156        self.route(ProcedureRoute::new(procedure))
157    }
158
159    #[cfg(feature = "discover")]
160    fn discover_procedures(mut self) -> Self {
161        for procedure in inventory::iter::<ErasedProcedure>().cloned() {
162            self = self.procedure(procedure);
163        }
164        self
165    }
166}
167
168#[derive(Debug, RefCast)]
169#[repr(transparent)]
170pub struct ProcedureSurrogate<A, R>(Procedure<A, R>);
171
172impl<A, R> ProcedureSurrogate<A, R> {
173    pub(crate) const fn new(v: Procedure<A, R>) -> Self {
174        Self(v)
175    }
176}
177
178impl<A, R> ProcedureSurrogate<A, R>
179where
180    A: Surrogated,
181    R: Surrogated,
182{
183    /// Invokes the procedure from the client side.
184    ///
185    /// # Panics
186    ///
187    /// Always panics; procedures can only be invoked from the client runtime.
188    #[allow(clippy::unused_async)]
189    pub async fn call(&self, _args: A::Surrogate) -> R::Surrogate {
190        panic!("procedures cannot be executed on the server");
191    }
192}
193
194crate::impl_surrogate!({A, R} Procedure<A, R>, ProcedureSurrogate<A, R>);
195crate::impl_surrogate_ref!({A, R} Procedure<A, R>, ProcedureSurrogate<A, R>);
196crate::impl_surrogate_mut!({A, R} Procedure<A, R>, ProcedureSurrogate<A, R>);
197
198impl<A, R> Serialize for ProcedureSurrogate<A, R> {
199    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
200    where
201        S: serde::Serializer,
202    {
203        #[derive(Serialize)]
204        struct TaggedProcedure {
205            t: &'static str,
206            id: ProcedureId,
207        }
208
209        TaggedProcedure {
210            t: "Procedure",
211            id: self.0.id(),
212        }
213        .serialize(serializer)
214    }
215}