Skip to main content

topcoat_runtime/
shard.rs

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