Skip to main content

webui_handler/plugin/
fast.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Shared FAST handler support and deprecated compatibility aliases.
5//!
6//! FAST hydration implementations live in `fast_v2` and `fast_v3`. This module
7//! owns behavior shared by both versions and keeps the original `plugin::fast`
8//! public path available.
9
10use crate::{ResponseWriter, Result};
11use serde_json::Value;
12use std::borrow::Cow;
13use webui_protocol::attrs::camel_to_kebab;
14
15pub use super::fast_v2::FastV2HydrationPlugin;
16
17/// Deprecated compatibility alias for the legacy `fast` handler plugin.
18///
19/// Use [`FastV2HydrationPlugin`] for explicit FAST 2 compatibility. FAST 3
20/// marker output lives in [`super::fast_v3::FastV3HydrationPlugin`].
21#[deprecated(
22    since = "0.0.11",
23    note = "use plugin::fast_v2::FastV2HydrationPlugin for FAST 2 compatibility or plugin::fast_v3::FastV3HydrationPlugin for FAST 3"
24)]
25pub type FastHydrationPlugin = super::fast_v2::FastV2HydrationPlugin;
26
27pub(crate) fn write_route_component_state(
28    state: &Value,
29    writer: &mut dyn ResponseWriter,
30) -> Result<()> {
31    let map = match state.as_object() {
32        Some(map) => map,
33        None => return Ok(()),
34    };
35
36    for (key, value) in map {
37        let value = match value {
38            Value::String(value) => Cow::Borrowed(value.as_str()),
39            Value::Number(value) => Cow::Owned(value.to_string()),
40            Value::Bool(true) => Cow::Borrowed("true"),
41            Value::Bool(false) => Cow::Borrowed("false"),
42            _ => continue,
43        };
44
45        let attr_name = camel_to_kebab(key);
46        writer.write(" ")?;
47        writer.write(&attr_name)?;
48        writer.write("=\"")?;
49        crate::route_renderer::write_escaped_state_attr(writer, value.as_ref())?;
50        writer.write("\"")?;
51    }
52
53    Ok(())
54}
55
56#[cfg(test)]
57mod tests {
58    #![allow(clippy::disallowed_methods)]
59
60    use super::*;
61    use crate::plugin::{fast_v3::FastV3HydrationPlugin, HandlerPlugin};
62
63    struct TestWriter {
64        output: String,
65    }
66
67    impl TestWriter {
68        fn new() -> Self {
69            Self {
70                output: String::new(),
71            }
72        }
73    }
74
75    impl ResponseWriter for TestWriter {
76        fn write(&mut self, content: &str) -> Result<()> {
77            self.output.push_str(content);
78            Ok(())
79        }
80
81        fn end(&mut self) -> Result<()> {
82            Ok(())
83        }
84    }
85
86    #[test]
87    #[allow(deprecated)]
88    fn test_fast_alias_uses_v2_markers() {
89        let mut plugin = FastHydrationPlugin::new();
90        plugin.push_scope();
91        let mut writer = TestWriter::new();
92        assert!(plugin
93            .on_binding_start("userName", false, &mut writer)
94            .is_ok());
95        assert_eq!(writer.output, "<!--fe-b$$start$$0$$userName$$fe-b-->");
96    }
97
98    fn render_route_state(plugin: &dyn HandlerPlugin, state: &Value) -> String {
99        let mut writer = TestWriter::new();
100        plugin
101            .write_route_component_state(state, &mut writer)
102            .unwrap();
103        writer.output
104    }
105
106    #[test]
107    fn fast_route_component_state_emits_scalar_attrs_only() {
108        let state = serde_json::json!({
109            "title": "A&B\"<>",
110            "count": 42,
111            "enabled": true,
112            "items": [{"name": "ignored"}],
113            "meta": {"nested": "ignored"},
114        });
115        let fast_v2 = FastV2HydrationPlugin::new();
116        let fast_v3 = FastV3HydrationPlugin::new();
117
118        for plugin in [
119            &fast_v2 as &dyn HandlerPlugin,
120            &fast_v3 as &dyn HandlerPlugin,
121        ] {
122            let output = render_route_state(plugin, &state);
123            assert!(
124                output.contains(r#" title="A&amp;B&quot;&lt;&gt;""#),
125                "escaped scalar string attr missing: {output}"
126            );
127            assert!(
128                output.contains(r#" count="42""#),
129                "numeric scalar attr missing: {output}"
130            );
131            assert!(
132                output.contains(r#" enabled="true""#),
133                "boolean scalar attr missing: {output}"
134            );
135            assert!(
136                !output.contains("items="),
137                "complex array attr must be omitted: {output}"
138            );
139            assert!(
140                !output.contains("meta="),
141                "complex object attr must be omitted: {output}"
142            );
143        }
144    }
145
146    #[test]
147    fn fast_route_component_state_ignores_non_object_values() {
148        let state = serde_json::json!(["not", "an", "object"]);
149        let fast_v2 = FastV2HydrationPlugin::new();
150        let fast_v3 = FastV3HydrationPlugin::new();
151
152        for plugin in [
153            &fast_v2 as &dyn HandlerPlugin,
154            &fast_v3 as &dyn HandlerPlugin,
155        ] {
156            assert_eq!(render_route_state(plugin, &state), "");
157        }
158    }
159}