Skip to main content

webserver_base/webserver/
state.rs

1//! The one context every handler receives.
2
3use std::sync::Arc;
4
5use crate::environment::Environment;
6
7use super::shutdown::Shutdown;
8
9#[cfg(feature = "templates")]
10use {
11    super::error::WebServerError,
12    crate::templates::{BaseTemplateData, PageTemplateData, TemplateData, TemplateRegistry},
13    axum::response::Html,
14    serde::Serialize,
15    std::collections::BTreeMap,
16    std::sync::LazyLock,
17};
18
19/// Stands in for the asset map when the `assets` kit is off.
20#[cfg(feature = "templates")]
21static NO_CACHE_BUSTER: LazyLock<BTreeMap<String, String>> = LazyLock::new(BTreeMap::new);
22
23/// Everything a handler can reach: the server's configuration and kits, plus
24/// the application's state as `S`.
25///
26/// One generic, not two — page data is passed to
27/// [`render`](WebServerState::render) per call. Cloning is an `Arc` bump.
28///
29/// ```ignore
30/// type Ctx = State<WebServerState<Arc<AppState>>>;
31/// ```
32#[derive(Debug)]
33pub struct WebServerState<S = ()>(Arc<Inner<S>>);
34
35impl<S> Clone for WebServerState<S> {
36    fn clone(&self) -> Self {
37        Self(Arc::clone(&self.0))
38    }
39}
40
41#[derive(Debug)]
42struct Inner<S> {
43    host: String,
44    port: u16,
45    environment: Environment,
46    shutdown: Shutdown,
47
48    #[cfg(feature = "templates")]
49    base: Option<BaseTemplateData>,
50    #[cfg(feature = "templates")]
51    templates: Option<TemplateRegistry<'static>>,
52    #[cfg(feature = "webserver")]
53    cache_buster: Option<crate::assets::CacheBuster>,
54    #[cfg(feature = "templates")]
55    frontend: Option<crate::templates::FrontendRuntime>,
56
57    app: S,
58}
59
60/// The pieces [`WebServerState::new`] assembles.
61pub(super) struct StateParts<S> {
62    pub(super) host: String,
63    pub(super) port: u16,
64    pub(super) environment: Environment,
65    pub(super) shutdown: Shutdown,
66    #[cfg(feature = "templates")]
67    pub(super) base: Option<BaseTemplateData>,
68    #[cfg(feature = "templates")]
69    pub(super) templates: Option<TemplateRegistry<'static>>,
70    #[cfg(feature = "webserver")]
71    pub(super) cache_buster: Option<crate::assets::CacheBuster>,
72    #[cfg(feature = "templates")]
73    pub(super) frontend: Option<crate::templates::FrontendRuntime>,
74    pub(super) app: S,
75}
76
77impl<S> WebServerState<S> {
78    pub(super) fn new(parts: StateParts<S>) -> Self {
79        Self(Arc::new(Inner {
80            host: parts.host,
81            port: parts.port,
82            environment: parts.environment,
83            shutdown: parts.shutdown,
84            #[cfg(feature = "templates")]
85            base: parts.base,
86            #[cfg(feature = "templates")]
87            templates: parts.templates,
88            #[cfg(feature = "webserver")]
89            cache_buster: parts.cache_buster,
90            #[cfg(feature = "templates")]
91            frontend: parts.frontend,
92            app: parts.app,
93        }))
94    }
95
96    /// The host this server bound to.
97    #[must_use]
98    pub fn host(&self) -> &str {
99        &self.0.host
100    }
101
102    /// The port this server bound to.
103    #[must_use]
104    pub fn port(&self) -> u16 {
105        self.0.port
106    }
107
108    /// Which deployment this is.
109    #[must_use]
110    pub fn environment(&self) -> Environment {
111        self.0.environment
112    }
113
114    /// The shutdown handle. Clone it into a socket loop to close cleanly:
115    ///
116    /// ```ignore
117    /// tokio::select! {
118    ///     message = socket.recv() => { /* … */ }
119    ///     () = state.shutdown().clone().recv() => {
120    ///         socket.send(Message::Close(None)).await.ok();
121    ///     }
122    /// }
123    /// ```
124    #[must_use]
125    pub fn shutdown(&self) -> &Shutdown {
126        &self.0.shutdown
127    }
128
129    /// The application's own state.
130    #[must_use]
131    pub fn app(&self) -> &S {
132        &self.0.app
133    }
134
135    /// The per-server template data, if `.templates(..)` was called.
136    #[cfg(feature = "templates")]
137    #[must_use]
138    pub fn base_template_data(&self) -> Option<&BaseTemplateData> {
139        self.0.base.as_ref()
140    }
141
142    /// The template registry, if `.templates(..)` was called.
143    #[cfg(feature = "templates")]
144    #[must_use]
145    pub fn templates(&self) -> Option<&TemplateRegistry<'static>> {
146        self.0.templates.as_ref()
147    }
148
149    /// The asset cache, if `.assets(..)` was called.
150    #[cfg(feature = "webserver")]
151    #[must_use]
152    pub fn cache_buster(&self) -> Option<&crate::assets::CacheBuster> {
153        self.0.cache_buster.as_ref()
154    }
155
156    /// The content-hashed path for an asset, or the original path when the
157    /// `assets` kit is not in use.
158    #[cfg(feature = "webserver")]
159    #[must_use]
160    pub fn asset(&self, original_asset_file_path: &str) -> String {
161        self.0.cache_buster.as_ref().map_or_else(
162            || original_asset_file_path.to_string(),
163            |cache_buster| cache_buster.get_file(original_asset_file_path),
164        )
165    }
166
167    /// The per-server frontend data, if `.frontend(..)` was called.
168    #[cfg(feature = "templates")]
169    #[must_use]
170    pub fn frontend_runtime(&self) -> Option<&crate::templates::FrontendRuntime> {
171        self.0.frontend.as_ref()
172    }
173
174    /// Renders `page` with `data` as its `{{app}}`. Pass `()` for none.
175    ///
176    /// # Errors
177    ///
178    /// [`WebServerError::TemplatesNotConfigured`] without `.templates(..)`, or
179    /// [`WebServerError::Template`] if the render fails.
180    #[cfg(feature = "templates")]
181    pub fn render<A>(
182        &self,
183        page: &PageTemplateData,
184        data: A,
185    ) -> Result<Html<String>, WebServerError>
186    where
187        A: Serialize,
188    {
189        let (Some(base), Some(registry)) = (self.0.base.as_ref(), self.0.templates.as_ref()) else {
190            return Err(WebServerError::TemplatesNotConfigured);
191        };
192
193        let template_data: TemplateData<'_, A> = TemplateData::assemble(
194            base,
195            page,
196            self.0.frontend.as_ref(),
197            self.0.environment,
198            self.cache_buster_map(),
199            data,
200        )?;
201
202        Ok(Html(registry.render(page.template(), &template_data)?))
203    }
204
205    /// The asset map, empty when the `assets` kit is off.
206    #[cfg(feature = "templates")]
207    fn cache_buster_map(&self) -> &BTreeMap<String, String> {
208        #[cfg(feature = "webserver")]
209        {
210            self.0
211                .cache_buster
212                .as_ref()
213                .map_or(&NO_CACHE_BUSTER, crate::assets::CacheBuster::cache)
214        }
215        #[cfg(not(feature = "webserver"))]
216        {
217            &NO_CACHE_BUSTER
218        }
219    }
220}