Skip to main content

webui_handler/plugin/
fast_v2.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Deprecated FAST 2 hydration plugin for the WebUI handler.
5//!
6//! Emits the legacy FAST 2 marker format used by the `fast` and `fast-v2`
7//! plugin names. FAST 3 hydration is implemented separately in `fast_v3`.
8//!
9//! ## FAST 2 Comment Format
10//!
11//! - **Binding start**: `<!--fe-b$$start$$INDEX$$NAME$$fe-b-->`
12//! - **Binding end**: `<!--fe-b$$end$$INDEX$$NAME$$fe-b-->`
13//! - **Repeat item start**: `<!--fe-repeat$$start$$INDEX$$fe-repeat-->`
14//! - **Repeat item end**: `<!--fe-repeat$$end$$INDEX$$fe-repeat-->`
15//! - **Single attribute binding**: ` data-fe-b-INDEX`
16//! - **Multiple attribute bindings**: ` data-fe-c-INDEX-COUNT`
17use super::HandlerPlugin;
18use crate::{HandlerError, ResponseWriter, Result};
19use serde_json::Value;
20use std::fmt::Write;
21use webui_protocol::FastElementData;
22
23// FAST 2 comment format constants
24const V2_BINDING_START_PREFIX: &str = "<!--fe-b$$start$$";
25const V2_BINDING_END_PREFIX: &str = "<!--fe-b$$end$$";
26const V2_BINDING_SUFFIX: &str = "$$fe-b-->";
27const V2_SEPARATOR: &str = "$$";
28const V2_REPEAT_START_PREFIX: &str = "<!--fe-repeat$$start$$";
29const V2_REPEAT_END_PREFIX: &str = "<!--fe-repeat$$end$$";
30const V2_REPEAT_SUFFIX: &str = "$$fe-repeat-->";
31const V2_ATTR_SINGLE_PREFIX: &str = " data-fe-b-";
32const V2_ATTR_MULTI_PREFIX: &str = " data-fe-c-";
33
34/// Deprecated FAST 2 hydration handler plugin.
35///
36/// Emits the legacy FAST 2 marker format used by the `fast` and `fast-v2`
37/// plugin names. New FAST 3 applications should use the separate
38/// `fast_v3::FastV3HydrationPlugin` implementation through `fast-v3` instead.
39pub struct FastV2HydrationPlugin {
40    /// Stack of local binding counters (one per scope).
41    /// The bottom of the stack is the root scope (disabled).
42    scopes: Vec<usize>,
43    /// Stack of binding indices for matching start/end pairs.
44    binding_stack: Vec<usize>,
45    /// Reusable buffer for formatting markers without allocation.
46    buffer: String,
47}
48
49impl FastV2HydrationPlugin {
50    /// Create a new deprecated FAST 2 hydration plugin.
51    /// The initial root scope is disabled — markers only emit in child scopes.
52    #[must_use]
53    pub fn new() -> Self {
54        Self {
55            // Root scope (index 0) is disabled — only scopes.len() > 1 are active.
56            scopes: vec![0],
57            binding_stack: Vec::with_capacity(8),
58            buffer: String::with_capacity(64),
59        }
60    }
61
62    /// Whether the current scope is active (not the root scope).
63    fn is_active(&self) -> bool {
64        self.scopes.len() > 1
65    }
66
67    /// Get the next binding index in the current scope, advancing the counter.
68    fn next_index(&mut self) -> usize {
69        if let Some(counter) = self.scopes.last_mut() {
70            let index = *counter;
71            *counter += 1;
72            index
73        } else {
74            0
75        }
76    }
77
78    /// Get the next binding index, advancing the counter by `count`.
79    fn next_index_n(&mut self, count: u32) -> usize {
80        if let Some(counter) = self.scopes.last_mut() {
81            let index = *counter;
82            *counter += count as usize;
83            index
84        } else {
85            0
86        }
87    }
88
89    /// Build a binding comment into the reusable buffer.
90    fn build_binding_comment(&mut self, prefix: &str, index: usize, name: &str) {
91        self.buffer.clear();
92        self.buffer.push_str(prefix);
93        let _ = write!(self.buffer, "{}", index);
94        self.buffer.push_str(V2_SEPARATOR);
95        self.buffer.push_str(name);
96        self.buffer.push_str(V2_BINDING_SUFFIX);
97    }
98
99    /// Build a repeat comment into the reusable buffer.
100    fn build_repeat_comment(&mut self, prefix: &str, index: usize) {
101        self.buffer.clear();
102        self.buffer.push_str(prefix);
103        let _ = write!(self.buffer, "{}", index);
104        self.buffer.push_str(V2_REPEAT_SUFFIX);
105    }
106
107    /// Build an attribute binding marker into the reusable buffer.
108    fn build_attribute_marker(&mut self, binding_index: usize, count: u32) {
109        self.buffer.clear();
110        if count == 1 {
111            self.buffer.push_str(V2_ATTR_SINGLE_PREFIX);
112            let _ = write!(self.buffer, "{}", binding_index);
113        } else {
114            self.buffer.push_str(V2_ATTR_MULTI_PREFIX);
115            let _ = write!(self.buffer, "{}-{}", binding_index, count);
116        }
117    }
118}
119
120impl Default for FastV2HydrationPlugin {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl HandlerPlugin for FastV2HydrationPlugin {
127    fn push_scope(&mut self) {
128        self.scopes.push(0);
129    }
130
131    fn pop_scope(&mut self) {
132        self.scopes.pop();
133    }
134
135    fn on_binding_start(
136        &mut self,
137        name: &str,
138        _raw: bool,
139        writer: &mut dyn ResponseWriter,
140    ) -> Result<()> {
141        if !self.is_active() {
142            return Ok(());
143        }
144        let index = self.next_index();
145        self.binding_stack.push(index);
146        self.build_binding_comment(V2_BINDING_START_PREFIX, index, name);
147        writer.write(&self.buffer)
148    }
149
150    fn on_binding_end(
151        &mut self,
152        name: &str,
153        _raw: bool,
154        writer: &mut dyn ResponseWriter,
155    ) -> Result<()> {
156        if !self.is_active() {
157            return Ok(());
158        }
159        let index = self.binding_stack.pop().unwrap_or(0);
160        self.build_binding_comment(V2_BINDING_END_PREFIX, index, name);
161        writer.write(&self.buffer)
162    }
163
164    fn on_repeat_item_start(
165        &mut self,
166        index: usize,
167        writer: &mut dyn ResponseWriter,
168    ) -> Result<()> {
169        if !self.is_active() {
170            return Ok(());
171        }
172        self.build_repeat_comment(V2_REPEAT_START_PREFIX, index);
173        writer.write(&self.buffer)
174    }
175
176    fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()> {
177        if !self.is_active() {
178            return Ok(());
179        }
180        self.build_repeat_comment(V2_REPEAT_END_PREFIX, index);
181        writer.write(&self.buffer)
182    }
183
184    fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()> {
185        if !self.is_active() {
186            return Ok(());
187        }
188        let decoded = FastElementData::decode(data).map_err(|error| {
189            HandlerError::PluginData(format!(
190                "FAST v2 hydration plugin expected 4 bytes of element data: {error}"
191            ))
192        })?;
193        if decoded.binding_count > 0 {
194            let binding_index = self.next_index_n(decoded.binding_count);
195            self.build_attribute_marker(binding_index, decoded.binding_count);
196            writer.write(&self.buffer)?;
197        }
198        Ok(())
199    }
200
201    /// FAST emits scalar attributes on route component elements.
202    /// Components read these via `@attr` and their connection lifecycle.
203    fn write_route_component_state(
204        &self,
205        state: &Value,
206        writer: &mut dyn ResponseWriter,
207    ) -> Result<()> {
208        super::fast::write_route_component_state(state, writer)
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    #![allow(clippy::disallowed_methods)]
215
216    use super::*;
217
218    struct TestWriter {
219        output: String,
220    }
221
222    impl TestWriter {
223        fn new() -> Self {
224            Self {
225                output: String::new(),
226            }
227        }
228    }
229
230    impl ResponseWriter for TestWriter {
231        fn write(&mut self, content: &str) -> Result<()> {
232            self.output.push_str(content);
233            Ok(())
234        }
235        fn end(&mut self) -> Result<()> {
236            Ok(())
237        }
238    }
239
240    #[test]
241    fn test_fast_v2_binding_marker_format() {
242        let mut plugin = FastV2HydrationPlugin::new();
243        plugin.push_scope();
244        let mut writer = TestWriter::new();
245        plugin
246            .on_binding_start("userName", false, &mut writer)
247            .unwrap();
248        plugin
249            .on_binding_end("userName", false, &mut writer)
250            .unwrap();
251        assert_eq!(
252            writer.output,
253            "<!--fe-b$$start$$0$$userName$$fe-b--><!--fe-b$$end$$0$$userName$$fe-b-->"
254        );
255    }
256
257    #[test]
258    fn test_fast_v2_binding_sequence_uses_indexes() {
259        let mut plugin = FastV2HydrationPlugin::new();
260        plugin.push_scope();
261        let mut writer = TestWriter::new();
262        plugin.on_binding_start("a", false, &mut writer).unwrap();
263        plugin.on_binding_end("a", false, &mut writer).unwrap();
264        writer.output.clear();
265        plugin.on_binding_start("b", false, &mut writer).unwrap();
266        assert_eq!(writer.output, "<!--fe-b$$start$$1$$b$$fe-b-->");
267    }
268
269    #[test]
270    fn test_fast_v2_repeat_marker_format() {
271        let mut plugin = FastV2HydrationPlugin::new();
272        plugin.push_scope();
273        let mut writer = TestWriter::new();
274        plugin.on_repeat_item_start(2, &mut writer).unwrap();
275        plugin.on_repeat_item_end(2, &mut writer).unwrap();
276        assert_eq!(
277            writer.output,
278            "<!--fe-repeat$$start$$2$$fe-repeat--><!--fe-repeat$$end$$2$$fe-repeat-->"
279        );
280    }
281
282    #[test]
283    fn test_fast_v2_attribute_marker_formats() {
284        let mut single = FastV2HydrationPlugin::new();
285        single.push_scope();
286        let mut writer = TestWriter::new();
287        let one = 1u32.to_le_bytes();
288        single.on_element_data(&one, &mut writer).unwrap();
289        assert_eq!(writer.output, " data-fe-b-0");
290
291        let mut multi = FastV2HydrationPlugin::new();
292        multi.push_scope();
293        writer.output.clear();
294        let three = 3u32.to_le_bytes();
295        multi.on_element_data(&three, &mut writer).unwrap();
296        assert_eq!(writer.output, " data-fe-c-0-3");
297    }
298
299    #[test]
300    fn test_fast_v2_attribute_count_advances_binding_index() {
301        let mut plugin = FastV2HydrationPlugin::new();
302        plugin.push_scope();
303        let mut writer = TestWriter::new();
304        let three = 3u32.to_le_bytes();
305        plugin.on_element_data(&three, &mut writer).unwrap();
306
307        writer.output.clear();
308        plugin.on_binding_start("next", false, &mut writer).unwrap();
309        assert_eq!(writer.output, "<!--fe-b$$start$$3$$next$$fe-b-->");
310    }
311
312    #[test]
313    fn test_fast_v2_root_scope_disabled() {
314        let mut plugin = FastV2HydrationPlugin::new();
315        let mut writer = TestWriter::new();
316        plugin.on_binding_start("x", false, &mut writer).unwrap();
317        plugin.on_binding_end("x", false, &mut writer).unwrap();
318        plugin.on_repeat_item_start(0, &mut writer).unwrap();
319        plugin.on_repeat_item_end(0, &mut writer).unwrap();
320        let data = 3u32.to_le_bytes();
321        plugin.on_element_data(&data, &mut writer).unwrap();
322        assert_eq!(writer.output, "");
323    }
324}