Skip to main content

topcoat_runtime/
shard.rs

1use std::{hash::Hash, pin::Pin};
2
3use topcoat_core::{
4    context::Cx,
5    error::{Error, Result},
6};
7
8use topcoat_router::{
9    Body, IntoResponse, Method, Path, PathBuf, Route, RouteFuture, RouterBuilder,
10};
11use topcoat_view::View;
12
13pub(crate) const SHARD_ROUTE_PREFIX: &str = "/_topcoat/shards";
14
15#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
16pub struct ShardId(&'static str);
17
18impl ShardId {
19    #[must_use]
20    pub const fn new(inner: &'static str) -> Self {
21        Self(inner)
22    }
23
24    #[must_use]
25    pub fn as_str(&self) -> &str {
26        self.0
27    }
28}
29
30pub type ShardRenderFn =
31    for<'cx> fn(
32        cx: &'cx Cx,
33        body: Body,
34    ) -> Pin<Box<dyn Future<Output = Result<View, Error>> + Send + 'cx>>;
35
36#[derive(Debug, Clone)]
37pub struct ErasedShard {
38    id: ShardId,
39    render: ShardRenderFn,
40}
41
42impl ErasedShard {
43    #[must_use]
44    pub const fn new(id: ShardId, render: ShardRenderFn) -> Self {
45        Self { id, render }
46    }
47
48    #[must_use]
49    pub fn id(&self) -> ShardId {
50        self.id
51    }
52
53    /// Renders the shard for an endpoint request, deserializing its arguments
54    /// from `body`.
55    ///
56    /// # Errors
57    ///
58    /// Propagates any error returned by the shard's render function, such as a
59    /// failure to deserialize the request body.
60    #[inline]
61    pub async fn render(&self, cx: &Cx, body: Body) -> Result<View> {
62        (self.render)(cx, body).await
63    }
64}
65
66#[cfg(feature = "discover")]
67inventory::collect!(ErasedShard);
68
69pub struct ShardRoute {
70    path: PathBuf,
71    shard: ErasedShard,
72}
73
74impl ShardRoute {
75    /// Builds the route that serves a shard.
76    pub fn new(shard: impl Into<ErasedShard>) -> Self {
77        let shard = shard.into();
78        Self {
79            path: Path::new(&format!("{SHARD_ROUTE_PREFIX}/{}", shard.id().as_str())).to_owned(),
80            shard,
81        }
82    }
83}
84
85impl Route for ShardRoute {
86    fn method(&self) -> Method {
87        // Avoids URL length limits for large parameters.
88        Method::POST
89    }
90
91    fn path(&self) -> &Path {
92        &self.path
93    }
94
95    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
96        Box::pin(async move {
97            let view = (self.shard.render)(cx, body).await?;
98            view.into_response(cx)
99        })
100    }
101}
102
103/// Registers shards on a [`RouterBuilder`].
104pub trait RouterBuilderShardExt {
105    /// Mounts a shard route.
106    #[must_use]
107    fn shard(self, shard: impl Into<ErasedShard>) -> Self;
108
109    /// Registers every shard linked into the binary.
110    #[cfg(feature = "discover")]
111    #[must_use]
112    fn discover_shards(self) -> Self;
113}
114
115impl RouterBuilderShardExt for RouterBuilder {
116    fn shard(self, shard: impl Into<ErasedShard>) -> Self {
117        self.route(ShardRoute::new(shard))
118    }
119
120    #[cfg(feature = "discover")]
121    fn discover_shards(mut self) -> Self {
122        for shard in inventory::iter::<ErasedShard>().cloned() {
123            self = self.shard(shard);
124        }
125        self
126    }
127}