topcoat_runtime/
procedure.rs1use 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#[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
32pub type ProcedureFuture<'cx> = Pin<Box<dyn Future<Output = Result<Response>> + Send + 'cx>>;
35
36pub trait Procedure: Send + Sync + 'static {
42 fn id(&self) -> ProcedureId;
44
45 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
62pub trait TypedProcedure: Procedure {
65 type Args: Surrogated;
67
68 type Output: Surrogated;
70}
71
72pub struct ProcedureRoute {
74 id: RouteId,
75 path: PathBuf,
76 procedure: Box<dyn Procedure>,
77}
78
79impl ProcedureRoute {
80 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
112pub trait RouterBuilderProcedureExt {
114 #[must_use]
116 fn procedure(self, procedure: impl Procedure) -> Self;
117
118 #[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
138pub 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 #[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}