Skip to main content

topcoat_runtime/
page.rs

1use serde::Deserialize;
2use topcoat_core::context::Cx;
3use topcoat_router::{
4    Body, Method, Methods, Path, PathBuf, Route, RouteFuture, RouteId,
5    content::Json,
6    error::rewrite,
7    request::{FromRequest, uri},
8};
9
10use crate::SignalValues;
11
12/// The route a page re-run is requested from, with the page's path and
13/// query appended.
14pub(crate) const PAGE_ROUTE_PREFIX: &str = "/_topcoat/runtime/pages";
15
16/// The body of a request re-running a page: the current values of the
17/// signals in the document.
18#[derive(Debug, Deserialize)]
19struct PageRerunRequest {
20    #[serde(default)]
21    signals: SignalValues,
22}
23
24/// A [`Route`] that re-runs a page with the signal values a client sends.
25///
26/// The browser runtime posts to the route with the page's path and query
27/// appended, and the route rewrites the request into a plain `GET` for the
28/// page with the signal values on its request context, so the page runs
29/// through its layouts and guards as if the client had requested it, and
30/// [`signal`](crate::signal) resumes from the values sent.
31///
32/// A page path is matched by a catch-all segment, which needs at least one
33/// segment, so the root page has a route of its own at the bare prefix.
34pub struct PageRerunRoute {
35    id: RouteId,
36    path: PathBuf,
37}
38
39impl PageRerunRoute {
40    /// The route re-running the root page.
41    #[must_use]
42    pub fn root() -> Self {
43        Self {
44            id: RouteId::new(),
45            path: Path::new(PAGE_ROUTE_PREFIX).to_owned(),
46        }
47    }
48
49    /// The route re-running every page below the root.
50    #[must_use]
51    pub fn nested() -> Self {
52        Self {
53            id: RouteId::new(),
54            path: Path::new(&format!("{PAGE_ROUTE_PREFIX}/{{*page}}")).to_owned(),
55        }
56    }
57}
58
59impl Route for PageRerunRoute {
60    fn id(&self) -> RouteId {
61        self.id
62    }
63
64    fn methods(&self) -> Methods<'_> {
65        // Avoids URL length limits for the signal values.
66        Methods::Only(&[Method::POST])
67    }
68
69    fn path(&self) -> &Path {
70        &self.path
71    }
72
73    fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
74        Box::pin(async move {
75            let Json(request) = Json::<PageRerunRequest>::from_request(cx, body).await?;
76
77            // The page's path is what follows the prefix, and its query is
78            // the request's own.
79            let uri = uri(cx);
80            let mut target = match uri.path().strip_prefix(PAGE_ROUTE_PREFIX) {
81                Some(path) if !path.is_empty() => path.to_owned(),
82                _ => String::from("/"),
83            };
84            if let Some(query) = uri.query() {
85                target.push('?');
86                target.push_str(query);
87            }
88
89            Err(rewrite(target, Body::empty())
90                .method(Method::GET)
91                .cx(cx.with(request.signals))
92                .into())
93        })
94    }
95}