Skip to main content

topcoat_runtime/
shard.rs

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