1use std::{collections::HashMap, fmt::Write};
2use stepflow_base::{ObjectStoreFiltered, IdError};
3use stepflow_data::{StateDataFiltered, var::{Var, VarId, StringVar, EmailVar, BoolVar}, value::StringValue};
4use super::{ActionResult, Action, ActionId, Step, ActionError};
5use crate::{render_template, EscapedString, HtmlEscapedString};
6
7
8#[derive(Debug)]
19pub struct HtmlFormConfig {
20 pub stringvar_html_template: String,
22
23 pub emailvar_html_template: String,
25
26 pub boolvar_html_template: String,
28
29 pub prefix_html_template: Option<String>,
37
38 pub wrap_tag: Option<String>, }
48
49impl HtmlFormConfig {
50 fn format_html_template(tag_template: &HtmlEscapedString, name_escaped: &HtmlEscapedString) -> String {
51 let mut params = HashMap::new();
52 params.insert("name", name_escaped);
53 render_template::<&HtmlEscapedString>(&tag_template, params)
54 }
55
56 fn valid_wraptag(&self) -> Option<&String> {
57 if let Some(wrap_tag) = &self.wrap_tag {
58 if !wrap_tag.is_empty() {
59 return Some(wrap_tag);
60 }
61 }
62 None
63 }
64
65 fn format_input_template(&self, html_template: &String, name_escaped: &HtmlEscapedString) -> Result<String, std::fmt::Error> {
66 let mut html = String::with_capacity(html_template.len() + name_escaped.len()); if let Some(wrap_tag) = self.valid_wraptag() {
70 if !wrap_tag.is_empty() {
71 write!(html, "<{}>", wrap_tag)?;
72 }
73 }
74
75 if let Some(prefix_html_template) = &self.prefix_html_template {
77 let prefix_html = Self::format_html_template(&HtmlEscapedString::already_escaped(prefix_html_template.to_owned()), name_escaped);
78 html.write_str(&prefix_html[..])?;
79 }
80
81 let input_html = Self::format_html_template(&HtmlEscapedString::already_escaped(html_template.to_owned()), name_escaped);
83 html.write_str(&input_html[..])?;
84
85 if let Some(wrap_tag) = self.valid_wraptag() {
87 write!(html, "</{}>", wrap_tag)?;
88 }
89
90
91 Ok(html)
92 }
93}
94
95impl Default for HtmlFormConfig {
96 fn default() -> Self {
97 HtmlFormConfig {
98 stringvar_html_template: "<input name='{{name}}' type='text' />".to_owned(),
99 emailvar_html_template: "<input name='{{name}}' type='email' />".to_owned(),
100 boolvar_html_template: "<input name='{{name}}' type='checkbox' />".to_owned(),
101 prefix_html_template: None,
102 wrap_tag: None,
103 }
104 }
105}
106
107
108#[derive(Debug)]
113pub struct HtmlFormAction {
114 id: ActionId,
115 html_config: HtmlFormConfig,
116}
117
118impl HtmlFormAction {
119 pub fn new(id: ActionId, html_config: HtmlFormConfig) -> Self {
121 HtmlFormAction {
122 id,
123 html_config,
124 }
125 }
126
127 pub fn boxed(self) -> Box<dyn Action + Sync + Send> {
128 Box::new(self)
129 }
130}
131
132impl Action for HtmlFormAction {
133 fn id(&self) -> &ActionId {
134 &self.id
135 }
136
137 fn start(&mut self, step: &Step, _step_name: Option<&str>, _step_data: &StateDataFiltered, vars: &ObjectStoreFiltered<Box<dyn Var + Send + Sync>, VarId>)
138 -> Result<ActionResult, ActionError>
139 {
140 const AVG_NAME_LEN: usize = 5;
141 let mut html = String::with_capacity(step.get_output_vars().len() * (self.html_config.stringvar_html_template.len() + AVG_NAME_LEN));
142 for var_id in step.get_output_vars().iter() {
143 let name = vars.name_from_id(var_id).ok_or_else(|| ActionError::VarId(IdError::IdHasNoName(var_id.clone())))?;
144 let name_escaped = HtmlEscapedString::from_unescaped(&(name.to_string())[..]);
145
146 let var = vars.get(var_id).ok_or_else(|| ActionError::VarId(IdError::IdMissing(var_id.clone())))?;
147 let html_template;
148 if var.is::<StringVar>() {
149 html_template = &self.html_config.stringvar_html_template;
150 } else if var.is::<EmailVar>() {
151 html_template = &self.html_config.emailvar_html_template;
152 } else if var.is::<BoolVar>() {
153 html_template = &self.html_config.boolvar_html_template;
154 } else {
155 return Err(ActionError::VarId(IdError::IdUnexpected(var_id.clone())));
158 }
159
160 self.html_config
161 .format_input_template(html_template, &name_escaped)
162 .and_then(|input_html| html.write_str(&input_html[..]))
163 .map_err(|_e| ActionError::Other)?;
164 }
165
166 let stringval = StringValue::try_new(html).map_err(|_e| ActionError::Other)?;
167 Ok(ActionResult::StartWith(stringval.boxed()))
168 }
169}
170
171
172
173#[cfg(test)]
174mod tests {
175 use std::collections::HashSet;
176 use super::{HtmlEscapedString, EscapedString, HtmlFormConfig, HtmlFormAction};
177 use stepflow_base::{ObjectStore, ObjectStoreFiltered};
178 use stepflow_data::{StateData, StateDataFiltered, var::{Var, VarId, EmailVar, StringVar}, value::StringValue};
179 use stepflow_step::{Step, StepId};
180 use stepflow_test_util::test_id;
181 use super::super::{ActionResult, Action, ActionId};
182
183 #[test]
184 fn html_format_input() {
185 let mut html_config: HtmlFormConfig = Default::default();
186 html_config.stringvar_html_template = "s({{name}},{{name}})".to_owned();
187 html_config.emailvar_html_template = "e({{name}},{{name}})".to_owned();
188
189 let escaped_n = HtmlEscapedString::from_unescaped("n");
191 let formatted = html_config.format_input_template(&html_config.stringvar_html_template, &escaped_n).unwrap();
192 assert_eq!(formatted, "s(n,n)");
193
194 html_config.prefix_html_template = Some("p({{name}})".to_owned());
196 let formatted_prefix = html_config.format_input_template(&html_config.stringvar_html_template, &escaped_n).unwrap();
197 assert_eq!(formatted_prefix, "p(n)s(n,n)");
198
199 html_config.wrap_tag = Some("div".to_owned());
201 let wrapped_prefix = html_config.format_input_template(&html_config.stringvar_html_template, &escaped_n).unwrap();
202 assert_eq!(wrapped_prefix, "<div>p(n)s(n,n)</div>");
203
204 html_config.wrap_tag = Some(String::new());
206 let wrapped_empty = html_config.format_input_template(&html_config.stringvar_html_template, &escaped_n).unwrap();
207 assert_eq!(wrapped_empty, "p(n)s(n,n)");
208 }
209
210 #[test]
211 fn simple_form() {
212 let var1 = StringVar::new(test_id!(VarId));
213 let var2 = EmailVar::new(test_id!(VarId));
214 let var_ids = vec![var1.id().clone(), var2.id().clone()];
215 let step = Step::new(StepId::new(4), None, var_ids.clone());
216
217 let state_data = StateData::new();
218 let var_filter = var_ids.iter().map(|id| id.clone()).collect::<HashSet<_>>();
219 let step_data_filtered = StateDataFiltered::new(&state_data, var_filter.clone());
220
221 let mut var_store: ObjectStore<Box<dyn Var + Send + Sync>, VarId> = ObjectStore::new();
222 var_store.register_named("var 1", var1.boxed()).unwrap();
223 var_store.register_named("var 2", var2.boxed()).unwrap();
224
225 let var_store_filtered = ObjectStoreFiltered::new(&var_store, var_filter);
226
227 let mut exec = HtmlFormAction::new(test_id!(ActionId), Default::default());
228 let action_result = exec.start(&step, None, &step_data_filtered, &var_store_filtered).unwrap();
229 if let ActionResult::StartWith(html) = action_result {
230 let html = html.downcast::<StringValue>().unwrap().val();
231 assert_eq!(html, "<input name='var 1' type='text' /><input name='var 2' type='email' />");
232 } else {
233 panic!("Did not get startwith value");
234 }
235
236 let mut html_config: HtmlFormConfig = Default::default();
238 html_config.prefix_html_template = Some("p({{name}})".to_owned());
239 html_config.stringvar_html_template = "l({{name}})s({{name}})".to_owned();
240 html_config.emailvar_html_template = "l({{name}})e({{name}})".to_owned();
241 let mut custom_exec = HtmlFormAction::new(test_id!(ActionId), html_config);
242 let custom_result = custom_exec.start(&step, None, &step_data_filtered, &var_store_filtered).unwrap();
243 if let ActionResult::StartWith(html) = custom_result {
244 let html = html.downcast::<StringValue>().unwrap().val();
245 assert_eq!(html, "p(var 1)l(var 1)s(var 1)p(var 2)l(var 2)e(var 2)");
246 } else {
247 panic!("Did not get startwith value");
248 }
249 }
250
251}