Skip to main content

webui/
server.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! High-level server helper for custom Rust servers using `webui-router`.
5//!
6//! This module provides [`serve_request`] which encapsulates:
7//! - Route parameter extraction from the URL
8//! - HTML SSR rendering via [`WebUIHandler`]
9//! - JSON partial responses for client-side navigation
10//! - Template inventory management
11//!
12//! # Example
13//!
14//! ```rust,ignore
15//! use webui::server::{serve_request, ServeRequest, ServeResponse};
16//!
17//! let request = ServeRequest {
18//!     path: "/email/thread-5",
19//!     accept_json: false,
20//!     inventory_hex: "",
21//! };
22//!
23//! match serve_request(&protocol, &handler, &state, "index.html", &request) {
24//!     Ok(ServeResponse::Html(html)) => { /* serve HTML */ }
25//!     Ok(ServeResponse::Json(json)) => { /* serve JSON */ }
26//!     Err(e) => { /* handle error */ }
27//! }
28//! ```
29
30use crate::{ResponseWriter, WebUIHandler};
31use webui_handler::route_handler;
32use webui_handler::RenderOptions;
33use webui_protocol::WebUIProtocol;
34
35/// A server request to be handled by [`serve_request`].
36pub struct ServeRequest<'a> {
37    /// The URL path (e.g., `"/email/thread-5"`, `"/folder/sent"`).
38    pub path: &'a str,
39    /// Whether the client accepts JSON (for partial navigation).
40    /// Check `Accept: application/json` in request headers.
41    pub accept_json: bool,
42    /// The client's current template inventory (hex bitmask).
43    /// Read from `X-WebUI-Inventory` request header. Empty string if not present.
44    pub inventory_hex: &'a str,
45}
46
47/// The response from [`serve_request`].
48pub enum ServeResponse {
49    /// Full HTML page for initial load or browser refresh.
50    Html(String),
51    /// JSON partial for client-side navigation via `webui-router`.
52    Json(serde_json::Value),
53}
54
55/// Handle a server request with automatic route handling.
56///
57/// For HTML requests: renders the full page with route-matched SSR,
58/// injects `__webui_state` and `__webui_templates`.
59///
60/// For JSON requests: returns a partial response with route-scoped state,
61/// needed templates, and inventory for the `webui-router` client.
62///
63/// # Arguments
64/// - `protocol` — The compiled WebUI protocol from [`build`](crate::build)
65/// - `handler` — The WebUI handler (with plugin configured)
66/// - `state` — The state JSON to render. For HTML requests, this should be
67///   the full app state. For JSON requests, the caller should provide
68///   route-scoped state (only what the target page component needs).
69/// - `request` — The incoming request details
70///
71/// # Route Parameters
72/// Route parameters (`:param` in route paths) are automatically extracted
73/// and injected into the state object.
74pub fn serve_request(
75    protocol: &WebUIProtocol,
76    handler: &WebUIHandler,
77    state: serde_json::Value,
78    entry: &str,
79    request: &ServeRequest<'_>,
80) -> Result<ServeResponse, String> {
81    // Extract route params and inject into state
82    let params = route_handler::collect_nested_route_params(protocol, entry, request.path);
83    let mut data = state;
84    if let Some(map) = data.as_object_mut() {
85        for (k, v) in &params {
86            map.insert(k.clone(), serde_json::Value::String(v.clone()));
87        }
88    }
89
90    if request.accept_json {
91        // JSON partial response for client-side navigation
92        let partial = route_handler::render_partial(
93            protocol,
94            data,
95            entry,
96            request.path,
97            request.inventory_hex,
98        );
99
100        let (needed, new_inv) = route_handler::get_needed_components_for_request(
101            protocol,
102            entry,
103            request.path,
104            request.inventory_hex,
105        );
106
107        let templates: Vec<serde_json::Value> = needed
108            .iter()
109            .filter_map(|name| {
110                protocol
111                    .components
112                    .get(name)
113                    .map(|c| c.template.as_str())
114                    .filter(|s| !s.is_empty())
115            })
116            .map(|t| serde_json::Value::String(t.to_string()))
117            .collect();
118
119        let mut resp = match partial {
120            serde_json::Value::Object(m) => m,
121            _ => serde_json::Map::new(),
122        };
123        resp.insert("templates".into(), serde_json::Value::Array(templates));
124        resp.insert("inventory".into(), serde_json::Value::String(new_inv));
125
126        Ok(ServeResponse::Json(serde_json::Value::Object(resp)))
127    } else {
128        // Full HTML SSR
129        let mut writer = MemWriter::with_capacity(131_072);
130        let opts = RenderOptions::new(entry, request.path);
131        handler
132            .handle(protocol, &data, &opts, &mut writer)
133            .map_err(|e| format!("render failed: {e}"))?;
134
135        let state_json =
136            serde_json::to_string(&data).map_err(|e| format!("serialize failed: {e}"))?;
137        let safe_json = state_json.replace("</", "<\\/");
138        let script = format!("<script>window.__webui_state={safe_json}</script>");
139        let html = writer.buf.replace("</body>", &format!("{script}</body>"));
140
141        Ok(ServeResponse::Html(html))
142    }
143}
144
145struct MemWriter {
146    buf: String,
147}
148
149impl MemWriter {
150    fn with_capacity(cap: usize) -> Self {
151        Self {
152            buf: String::with_capacity(cap),
153        }
154    }
155}
156
157impl ResponseWriter for MemWriter {
158    fn write(&mut self, content: &str) -> webui_handler::Result<()> {
159        self.buf.push_str(content);
160        Ok(())
161    }
162
163    fn end(&mut self) -> webui_handler::Result<()> {
164        Ok(())
165    }
166}