webui_handler/plugin/
fast.rs1use 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(
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&B"<>""#),
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}