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