1use crate::bound_field::BoundField;
2use crate::field::{FieldError, FormField};
3use crate::wasm_compat::ValidationRule;
4use std::collections::{HashMap, HashSet};
5use std::ops::Index;
6
7fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
13 use sha2::{Digest, Sha256};
14 use subtle::ConstantTimeEq;
15
16 let hash_a = Sha256::digest(a);
17 let hash_b = Sha256::digest(b);
18 hash_a.ct_eq(&hash_b).into()
19}
20
21#[derive(Debug, thiserror::Error)]
23pub enum FormError {
24 #[error("Field error in {field}: {error}")]
26 Field {
27 field: String,
29 error: FieldError,
31 },
32 #[error("Validation error: {0}")]
34 Validation(String),
35 #[error("No model instance available for save operation")]
37 NoInstance,
38}
39
40pub type FormResult<T> = Result<T, FormError>;
42
43type CleanFunction =
44 Box<dyn Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync>;
45type FieldCleanFunction =
46 Box<dyn Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync>;
47
48pub const ALL_FIELDS_KEY: &str = "_all";
53
54pub struct Form {
56 fields: Vec<Box<dyn FormField>>,
57 data: HashMap<String, serde_json::Value>,
58 cleaned_data: HashMap<String, serde_json::Value>,
59 cleaned_field_names: HashSet<String>,
60 initial: HashMap<String, serde_json::Value>,
61 errors: HashMap<String, Vec<String>>,
62 is_bound: bool,
63 validation_complete: bool,
64 clean_functions: Vec<CleanFunction>,
65 field_clean_functions: HashMap<String, FieldCleanFunction>,
66 prefix: String,
67 validation_rules: Vec<ValidationRule>,
71 csrf_token: Option<String>,
73 csrf_enabled: bool,
75}
76
77impl Form {
78 pub fn new() -> Self {
90 Self {
91 fields: vec![],
92 data: HashMap::new(),
93 cleaned_data: HashMap::new(),
94 cleaned_field_names: HashSet::new(),
95 initial: HashMap::new(),
96 errors: HashMap::new(),
97 is_bound: false,
98 validation_complete: false,
99 clean_functions: vec![],
100 field_clean_functions: HashMap::new(),
101 prefix: String::new(),
102 validation_rules: vec![],
103 csrf_token: None,
104 csrf_enabled: false,
105 }
106 }
107 pub fn with_initial(initial: HashMap<String, serde_json::Value>) -> Self {
123 Self {
124 fields: vec![],
125 data: HashMap::new(),
126 cleaned_data: HashMap::new(),
127 cleaned_field_names: HashSet::new(),
128 initial,
129 errors: HashMap::new(),
130 is_bound: false,
131 validation_complete: false,
132 clean_functions: vec![],
133 field_clean_functions: HashMap::new(),
134 prefix: String::new(),
135 validation_rules: vec![],
136 csrf_token: None,
137 csrf_enabled: false,
138 }
139 }
140 pub fn with_prefix(prefix: String) -> Self {
152 Self {
153 fields: vec![],
154 data: HashMap::new(),
155 cleaned_data: HashMap::new(),
156 cleaned_field_names: HashSet::new(),
157 initial: HashMap::new(),
158 errors: HashMap::new(),
159 is_bound: false,
160 validation_complete: false,
161 clean_functions: vec![],
162 field_clean_functions: HashMap::new(),
163 prefix,
164 validation_rules: vec![],
165 csrf_token: None,
166 csrf_enabled: false,
167 }
168 }
169 pub fn add_field(&mut self, field: Box<dyn FormField>) {
182 self.fields.push(field);
183 }
184 pub fn bind(&mut self, data: HashMap<String, serde_json::Value>) {
201 self.cleaned_data = data.clone();
202 self.cleaned_field_names.clear();
203 self.data = data;
204 self.is_bound = true;
205 self.validation_complete = false;
206 }
207 pub fn is_valid(&mut self) -> bool {
228 if !self.is_bound {
229 return false;
230 }
231
232 self.validation_complete = false;
233 self.errors.clear();
234 self.cleaned_data = self.data.clone();
235 self.cleaned_field_names.clear();
236
237 if !self.validate_csrf() {
239 self.errors
240 .entry(ALL_FIELDS_KEY.to_string())
241 .or_default()
242 .push("CSRF token missing or incorrect.".to_string());
243 return false;
244 }
245
246 for field in &self.fields {
247 let value = self.data_for_field(field.name());
248
249 match field.clean(value) {
250 Ok(mut cleaned) => {
251 if let Some(field_clean) = self.field_clean_functions.get(field.name()) {
253 match field_clean(&cleaned) {
254 Ok(further_cleaned) => {
255 cleaned = further_cleaned;
256 }
257 Err(e) => {
258 self.errors
259 .entry(field.name().to_string())
260 .or_default()
261 .push(e.to_string());
262 continue;
263 }
264 }
265 }
266 self.cleaned_field_names.insert(field.name().to_string());
267 let submitted_name = self.add_prefix_to_field_name(field.name());
268 if submitted_name != field.name()
269 && !self.cleaned_field_names.contains(&submitted_name)
270 {
271 self.cleaned_data.remove(&submitted_name);
272 }
273 self.cleaned_data.insert(field.name().to_string(), cleaned);
274 }
275 Err(e) => {
276 self.errors
277 .entry(field.name().to_string())
278 .or_default()
279 .push(e.to_string());
280 }
281 }
282 }
283
284 for clean_fn in &self.clean_functions {
286 if let Err(e) = clean_fn(&self.cleaned_data) {
287 match e {
288 FormError::Field { field, error } => {
289 self.errors
290 .entry(field)
291 .or_default()
292 .push(error.to_string());
293 }
294 FormError::Validation(msg) => {
295 self.errors
296 .entry(ALL_FIELDS_KEY.to_string())
297 .or_default()
298 .push(msg);
299 }
300 FormError::NoInstance => {
301 self.errors
302 .entry(ALL_FIELDS_KEY.to_string())
303 .or_default()
304 .push(e.to_string());
305 }
306 }
307 }
308 }
309
310 self.validation_complete = true;
311 self.errors.is_empty()
312 }
313 pub fn cleaned_data(&self) -> &HashMap<String, serde_json::Value> {
315 &self.cleaned_data
316 }
317 pub fn errors(&self) -> &HashMap<String, Vec<String>> {
319 &self.errors
320 }
321 pub fn add_error(&mut self, field_name: impl Into<String>, message: impl Into<String>) {
326 self.errors
327 .entry(field_name.into())
328 .or_default()
329 .push(message.into());
330 }
331 pub fn is_bound(&self) -> bool {
333 self.is_bound
334 }
335 pub fn fields(&self) -> &[Box<dyn FormField>] {
337 &self.fields
338 }
339 pub fn initial(&self) -> &HashMap<String, serde_json::Value> {
341 &self.initial
342 }
343 pub fn set_initial(&mut self, initial: HashMap<String, serde_json::Value>) {
358 self.initial = initial;
359 }
360 pub fn has_changed(&self) -> bool {
382 if !self.is_bound {
383 return false;
384 }
385
386 for field in &self.fields {
387 let initial_val = self.initial.get(field.name());
388 let data_val = if self.validation_complete {
389 self.cleaned_data_for_field(field.name())
390 .or_else(|| self.data_for_field(field.name()))
391 } else {
392 self.data_for_field(field.name())
393 };
394 if field.has_changed(initial_val, data_val) {
395 return true;
396 }
397 }
398 false
399 }
400 pub fn get_field(&self, name: &str) -> Option<&dyn FormField> {
402 self.fields
403 .iter()
404 .find(|f| f.name() == name)
405 .map(|f| f.as_ref())
406 }
407 pub fn remove_field(&mut self, name: &str) -> Option<Box<dyn FormField>> {
409 let pos = self.fields.iter().position(|f| f.name() == name)?;
410 Some(self.fields.remove(pos))
411 }
412 pub fn field_count(&self) -> usize {
414 self.fields.len()
415 }
416 pub fn add_clean_function<F>(&mut self, f: F)
435 where
436 F: Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync + 'static,
437 {
438 self.clean_functions.push(Box::new(f));
439 }
440 pub fn add_field_clean_function<F>(&mut self, field_name: &str, f: F)
462 where
463 F: Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync + 'static,
464 {
465 self.field_clean_functions
466 .insert(field_name.to_string(), Box::new(f));
467 }
468
469 pub fn validation_rules(&self) -> &[ValidationRule] {
475 &self.validation_rules
476 }
477
478 pub fn add_min_length_validator(
501 &mut self,
502 field_name: impl Into<String>,
503 min: usize,
504 error_message: impl Into<String>,
505 ) {
506 self.validation_rules.push(ValidationRule::MinLength {
507 field_name: field_name.into(),
508 min,
509 error_message: error_message.into(),
510 });
511 }
512
513 pub fn add_max_length_validator(
526 &mut self,
527 field_name: impl Into<String>,
528 max: usize,
529 error_message: impl Into<String>,
530 ) {
531 self.validation_rules.push(ValidationRule::MaxLength {
532 field_name: field_name.into(),
533 max,
534 error_message: error_message.into(),
535 });
536 }
537
538 pub fn add_pattern_validator(
551 &mut self,
552 field_name: impl Into<String>,
553 pattern: impl Into<String>,
554 error_message: impl Into<String>,
555 ) {
556 self.validation_rules.push(ValidationRule::Pattern {
557 field_name: field_name.into(),
558 pattern: pattern.into(),
559 error_message: error_message.into(),
560 });
561 }
562
563 pub fn add_min_value_validator(
576 &mut self,
577 field_name: impl Into<String>,
578 min: f64,
579 error_message: impl Into<String>,
580 ) {
581 self.validation_rules.push(ValidationRule::MinValue {
582 field_name: field_name.into(),
583 min,
584 error_message: error_message.into(),
585 });
586 }
587
588 pub fn add_max_value_validator(
601 &mut self,
602 field_name: impl Into<String>,
603 max: f64,
604 error_message: impl Into<String>,
605 ) {
606 self.validation_rules.push(ValidationRule::MaxValue {
607 field_name: field_name.into(),
608 max,
609 error_message: error_message.into(),
610 });
611 }
612
613 pub fn add_email_validator(
626 &mut self,
627 field_name: impl Into<String>,
628 error_message: impl Into<String>,
629 ) {
630 self.validation_rules.push(ValidationRule::Email {
631 field_name: field_name.into(),
632 error_message: error_message.into(),
633 });
634 }
635
636 pub fn add_url_validator(
649 &mut self,
650 field_name: impl Into<String>,
651 error_message: impl Into<String>,
652 ) {
653 self.validation_rules.push(ValidationRule::Url {
654 field_name: field_name.into(),
655 error_message: error_message.into(),
656 });
657 }
658
659 pub fn add_fields_equal_validator(
683 &mut self,
684 field_names: Vec<String>,
685 error_message: impl Into<String>,
686 target_field: Option<String>,
687 ) {
688 self.validation_rules.push(ValidationRule::FieldsEqual {
689 field_names,
690 error_message: error_message.into(),
691 target_field,
692 });
693 }
694
695 pub fn add_validator_rule(
732 &mut self,
733 field_name: impl Into<String>,
734 validator_id: impl Into<String>,
735 params: serde_json::Value,
736 error_message: impl Into<String>,
737 ) {
738 self.validation_rules.push(ValidationRule::ValidatorRef {
739 field_name: field_name.into(),
740 validator_id: validator_id.into(),
741 params,
742 error_message: error_message.into(),
743 });
744 }
745
746 pub fn add_date_range_validator(
765 &mut self,
766 start_field: impl Into<String>,
767 end_field: impl Into<String>,
768 error_message: Option<String>,
769 ) {
770 let start = start_field.into();
771 let end = end_field.into();
772 let message = error_message
773 .unwrap_or_else(|| "End date must be after or equal to start date".to_string());
774
775 self.validation_rules.push(ValidationRule::DateRange {
776 start_field: start,
777 end_field: end.clone(),
778 error_message: message,
779 target_field: Some(end),
780 });
781 }
782
783 pub fn add_numeric_range_validator(
802 &mut self,
803 min_field: impl Into<String>,
804 max_field: impl Into<String>,
805 error_message: Option<String>,
806 ) {
807 let min = min_field.into();
808 let max = max_field.into();
809 let message = error_message.unwrap_or_else(|| {
810 "Maximum value must be greater than or equal to minimum value".to_string()
811 });
812
813 self.validation_rules.push(ValidationRule::NumericRange {
814 min_field: min,
815 max_field: max.clone(),
816 error_message: message,
817 target_field: Some(max),
818 });
819 }
820 pub fn set_csrf_token(&mut self, token: String) {
839 self.csrf_token = Some(token);
840 self.csrf_enabled = true;
841 }
842
843 pub fn csrf_enabled(&self) -> bool {
845 self.csrf_enabled
846 }
847
848 pub fn csrf_token(&self) -> Option<&str> {
850 self.csrf_token.as_deref()
851 }
852
853 fn validate_csrf(&self) -> bool {
857 if !self.csrf_enabled {
858 return true;
859 }
860
861 let expected = match &self.csrf_token {
862 Some(t) => t,
863 None => return false,
864 };
865
866 let submitted = self
867 .data
868 .get("csrfmiddlewaretoken")
869 .and_then(|v| v.as_str());
870
871 match submitted {
872 Some(token) => {
873 constant_time_eq(token.as_bytes(), expected.as_bytes())
875 }
876 None => false,
877 }
878 }
879
880 pub fn prefix(&self) -> &str {
882 &self.prefix
883 }
884 pub fn set_prefix(&mut self, prefix: String) {
886 self.prefix = prefix;
887 }
888 pub fn add_prefix_to_field_name(&self, field_name: &str) -> String {
890 if self.prefix.is_empty() {
891 field_name.to_string()
892 } else {
893 format!("{}-{}", self.prefix, field_name)
894 }
895 }
896
897 fn data_for_field(&self, field_name: &str) -> Option<&serde_json::Value> {
898 if self.prefix.is_empty() {
899 self.data.get(field_name)
900 } else {
901 let prefixed_name = self.add_prefix_to_field_name(field_name);
902 self.data.get(&prefixed_name)
903 }
904 }
905
906 fn cleaned_data_for_field(&self, field_name: &str) -> Option<&serde_json::Value> {
907 if !self.cleaned_field_names.contains(field_name) {
908 return None;
909 }
910
911 self.cleaned_data.get(field_name)
912 }
913 pub fn render_css_media(&self, css_files: &[&str]) -> String {
932 use crate::field::escape_attribute;
933 let mut html = String::new();
934 for path in css_files {
935 html.push_str(&format!(
936 "<link rel=\"stylesheet\" href=\"{}\" />\n",
937 escape_attribute(path)
938 ));
939 }
940 html
941 }
942
943 pub fn render_js_media(&self, js_files: &[&str]) -> String {
962 use crate::field::escape_attribute;
963 let mut html = String::new();
964 for path in js_files {
965 html.push_str(&format!(
966 "<script src=\"{}\"></script>\n",
967 escape_attribute(path)
968 ));
969 }
970 html
971 }
972
973 pub fn get_bound_field<'a>(&'a self, name: &str) -> Option<BoundField<'a>> {
975 let field = self.get_field(name)?;
976 let data = if field.is_sensitive()
977 && self.validation_complete
978 && self.cleaned_field_names.contains(field.name())
979 {
980 self.cleaned_data_for_field(name)
981 } else {
982 self.data_for_field(name)
983 };
984 let errors = self.errors.get(name).map(|e| e.as_slice()).unwrap_or(&[]);
985
986 Some(BoundField::new(
987 "form".to_string(),
988 field,
989 data,
990 errors,
991 &self.prefix,
992 ))
993 }
994}
995
996impl Default for Form {
997 fn default() -> Self {
998 Self::new()
999 }
1000}
1001
1002impl Form {
1018 #[allow(clippy::borrowed_box)]
1020 pub fn get(&self, name: &str) -> Option<&Box<dyn FormField>> {
1022 self.fields.iter().find(|f| f.name() == name)
1023 }
1024}
1025
1026impl Index<&str> for Form {
1027 type Output = Box<dyn FormField>;
1028
1029 fn index(&self, name: &str) -> &Self::Output {
1030 self.get(name)
1031 .unwrap_or_else(|| panic!("Field '{}' not found", name))
1032 }
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037 use super::*;
1038 use crate::fields::{CharField, IntegerField};
1039 use rstest::rstest;
1040 use serde_json::json;
1041
1042 #[test]
1043 fn test_form_validation() {
1044 let mut form = Form::new();
1045
1046 let mut name_field = CharField::new("name".to_string());
1047 name_field.max_length = Some(50);
1048 form.add_field(Box::new(name_field));
1049
1050 let mut data = HashMap::new();
1051 data.insert("name".to_string(), serde_json::json!("John Doe"));
1052
1053 form.bind(data);
1054 assert!(form.is_valid());
1055 assert!(form.errors().is_empty());
1056 }
1057
1058 #[test]
1059 fn test_form_validation_error() {
1060 let mut form = Form::new();
1061
1062 let mut name_field = CharField::new("name".to_string());
1063 name_field.max_length = Some(5);
1064 form.add_field(Box::new(name_field));
1065
1066 let mut data = HashMap::new();
1067 data.insert("name".to_string(), serde_json::json!("Very Long Name"));
1068
1069 form.bind(data);
1070 assert!(!form.is_valid());
1071 assert!(!form.errors().is_empty());
1072 }
1073
1074 #[test]
1077 fn test_form_basic() {
1078 use crate::fields::CharField;
1080
1081 let mut form = Form::new();
1082 form.add_field(Box::new(CharField::new("first_name".to_string())));
1083 form.add_field(Box::new(CharField::new("last_name".to_string())));
1084
1085 let mut data = HashMap::new();
1086 data.insert("first_name".to_string(), serde_json::json!("John"));
1087 data.insert("last_name".to_string(), serde_json::json!("Lennon"));
1088
1089 form.bind(data);
1090
1091 assert!(form.is_bound());
1092 assert!(form.is_valid());
1093 assert!(form.errors().is_empty());
1094
1095 let cleaned = form.cleaned_data();
1097 assert_eq!(
1098 cleaned.get("first_name").unwrap(),
1099 &serde_json::json!("John")
1100 );
1101 assert_eq!(
1102 cleaned.get("last_name").unwrap(),
1103 &serde_json::json!("Lennon")
1104 );
1105 }
1106
1107 #[test]
1108 fn test_form_missing_required_fields() {
1109 use crate::fields::CharField;
1111
1112 let mut form = Form::new();
1113 form.add_field(Box::new(CharField::new("username".to_string()).required()));
1114 form.add_field(Box::new(CharField::new("email".to_string()).required()));
1115
1116 let data = HashMap::new(); form.bind(data);
1119
1120 assert!(form.is_bound());
1121 assert!(!form.is_valid());
1122 assert!(form.errors().contains_key("username"));
1123 assert!(form.errors().contains_key("email"));
1124 }
1125
1126 #[test]
1127 fn test_form_optional_fields() {
1128 use crate::fields::CharField;
1130
1131 let mut form = Form::new();
1132
1133 let username_field = CharField::new("username".to_string());
1134 form.add_field(Box::new(username_field));
1135
1136 let mut bio_field = CharField::new("bio".to_string());
1137 bio_field.required = false;
1138 form.add_field(Box::new(bio_field));
1139
1140 let mut data = HashMap::new();
1141 data.insert("username".to_string(), serde_json::json!("john"));
1142 form.bind(data);
1145
1146 assert!(form.is_bound());
1147 assert!(form.is_valid());
1148 assert!(form.errors().is_empty());
1149 }
1150
1151 #[test]
1152 fn test_form_unbound() {
1153 use crate::fields::CharField;
1155
1156 let mut form = Form::new();
1157 form.add_field(Box::new(CharField::new("name".to_string())));
1158
1159 assert!(!form.is_bound());
1160 assert!(!form.is_valid()); }
1162
1163 #[test]
1164 fn test_form_extra_data() {
1165 use crate::fields::CharField;
1167
1168 let mut form = Form::new();
1169 form.add_field(Box::new(CharField::new("name".to_string())));
1170
1171 let mut data = HashMap::new();
1172 data.insert("name".to_string(), serde_json::json!("John"));
1173 data.insert(
1174 "extra_field".to_string(),
1175 serde_json::json!("should be ignored"),
1176 );
1177
1178 form.bind(data);
1179
1180 assert!(form.is_valid());
1181 let cleaned = form.cleaned_data();
1182 assert_eq!(cleaned.get("name").unwrap(), &serde_json::json!("John"));
1183 assert!(cleaned.contains_key("extra_field"));
1185 }
1186
1187 #[test]
1188 fn test_forms_form_multiple_fields() {
1189 use crate::fields::{CharField, IntegerField};
1191
1192 let mut form = Form::new();
1193 form.add_field(Box::new(CharField::new("username".to_string())));
1194
1195 let mut age_field = IntegerField::new("age".to_string());
1196 age_field.min_value = Some(0);
1197 age_field.max_value = Some(150);
1198 form.add_field(Box::new(age_field));
1199
1200 let mut data = HashMap::new();
1201 data.insert("username".to_string(), serde_json::json!("alice"));
1202 data.insert("age".to_string(), serde_json::json!(30));
1203
1204 form.bind(data);
1205
1206 assert!(form.is_valid());
1207 assert!(form.errors().is_empty());
1208 }
1209
1210 #[test]
1211 fn test_form_multiple_fields_invalid() {
1212 use crate::fields::{CharField, IntegerField};
1214
1215 let mut form = Form::new();
1216
1217 let mut username_field = CharField::new("username".to_string());
1218 username_field.min_length = Some(3);
1219 form.add_field(Box::new(username_field));
1220
1221 let mut age_field = IntegerField::new("age".to_string());
1222 age_field.min_value = Some(0);
1223 age_field.max_value = Some(150);
1224 form.add_field(Box::new(age_field));
1225
1226 let mut data = HashMap::new();
1227 data.insert("username".to_string(), serde_json::json!("ab")); data.insert("age".to_string(), serde_json::json!(200)); form.bind(data);
1231
1232 assert!(!form.is_valid());
1233 assert!(form.errors().contains_key("username"));
1234 assert!(form.errors().contains_key("age"));
1235 }
1236
1237 #[test]
1238 fn test_form_multiple_instances() {
1239 use crate::fields::CharField;
1241
1242 let mut form1 = Form::new();
1243 form1.add_field(Box::new(CharField::new("name".to_string())));
1244
1245 let mut form2 = Form::new();
1246 form2.add_field(Box::new(CharField::new("name".to_string())));
1247
1248 let mut data1 = HashMap::new();
1249 data1.insert("name".to_string(), serde_json::json!("Form1"));
1250 form1.bind(data1);
1251
1252 let mut data2 = HashMap::new();
1253 data2.insert("name".to_string(), serde_json::json!("Form2"));
1254 form2.bind(data2);
1255
1256 assert!(form1.is_valid());
1257 assert!(form2.is_valid());
1258
1259 assert_eq!(
1260 form1.cleaned_data().get("name").unwrap(),
1261 &serde_json::json!("Form1")
1262 );
1263 assert_eq!(
1264 form2.cleaned_data().get("name").unwrap(),
1265 &serde_json::json!("Form2")
1266 );
1267 }
1268
1269 #[test]
1270 fn test_form_with_initial_data() {
1271 let mut initial = HashMap::new();
1272 initial.insert("name".to_string(), serde_json::json!("Initial Name"));
1273 initial.insert("age".to_string(), serde_json::json!(25));
1274
1275 let mut form = Form::with_initial(initial);
1276
1277 let name_field = CharField::new("name".to_string());
1278 form.add_field(Box::new(name_field));
1279
1280 let age_field = crate::IntegerField::new("age".to_string());
1281 form.add_field(Box::new(age_field));
1282
1283 assert_eq!(
1284 form.initial().get("name").unwrap(),
1285 &serde_json::json!("Initial Name")
1286 );
1287 assert_eq!(form.initial().get("age").unwrap(), &serde_json::json!(25));
1288 }
1289
1290 #[test]
1291 fn test_form_has_changed() {
1292 let mut initial = HashMap::new();
1293 initial.insert("name".to_string(), serde_json::json!("John"));
1294
1295 let mut form = Form::with_initial(initial);
1296
1297 let name_field = CharField::new("name".to_string());
1298 form.add_field(Box::new(name_field));
1299
1300 let mut data1 = HashMap::new();
1302 data1.insert("name".to_string(), serde_json::json!("John"));
1303 form.bind(data1);
1304 assert!(!form.has_changed());
1305
1306 let mut data2 = HashMap::new();
1308 data2.insert("name".to_string(), serde_json::json!("Jane"));
1309 form.bind(data2);
1310 assert!(form.has_changed());
1311 }
1312
1313 #[test]
1314 fn test_form_index_access() {
1315 let mut form = Form::new();
1316
1317 let name_field = CharField::new("name".to_string());
1318 form.add_field(Box::new(name_field));
1319
1320 let field = &form["name"];
1321 assert_eq!(field.name(), "name");
1322 }
1323
1324 #[test]
1325 #[should_panic(expected = "Field 'nonexistent' not found")]
1326 fn test_form_index_access_nonexistent() {
1327 let form = Form::new();
1328 let _ = &form["nonexistent"];
1329 }
1330
1331 #[test]
1332 fn test_form_get_field() {
1333 let mut form = Form::new();
1334
1335 let name_field = CharField::new("name".to_string());
1336 form.add_field(Box::new(name_field));
1337
1338 assert!(form.get_field("name").is_some());
1339 assert!(form.get_field("nonexistent").is_none());
1340 }
1341
1342 #[test]
1343 fn test_form_remove_field() {
1344 let mut form = Form::new();
1345
1346 let name_field = CharField::new("name".to_string());
1347 form.add_field(Box::new(name_field));
1348
1349 assert_eq!(form.field_count(), 1);
1350
1351 let removed = form.remove_field("name");
1352 assert!(removed.is_some());
1353 assert_eq!(form.field_count(), 0);
1354
1355 let not_removed = form.remove_field("nonexistent");
1356 assert!(not_removed.is_none());
1357 }
1358
1359 #[test]
1360 fn test_form_custom_validation() {
1361 let mut form = Form::new();
1362
1363 let mut password_field = CharField::new("password".to_string());
1364 password_field.min_length = Some(8);
1365 form.add_field(Box::new(password_field));
1366
1367 let mut confirm_field = CharField::new("confirm".to_string());
1368 confirm_field.min_length = Some(8);
1369 form.add_field(Box::new(confirm_field));
1370
1371 form.add_clean_function(|data| {
1373 let password = data.get("password").and_then(|v| v.as_str());
1374 let confirm = data.get("confirm").and_then(|v| v.as_str());
1375
1376 if password != confirm {
1377 return Err(FormError::Validation("Passwords do not match".to_string()));
1378 }
1379
1380 Ok(())
1381 });
1382
1383 let mut data1 = HashMap::new();
1385 data1.insert("password".to_string(), serde_json::json!("secret123"));
1386 data1.insert("confirm".to_string(), serde_json::json!("secret123"));
1387 form.bind(data1);
1388 assert!(form.is_valid());
1389
1390 let mut data2 = HashMap::new();
1392 data2.insert("password".to_string(), serde_json::json!("secret123"));
1393 data2.insert("confirm".to_string(), serde_json::json!("different"));
1394 form.bind(data2);
1395 assert!(!form.is_valid());
1396 assert!(form.errors().contains_key(ALL_FIELDS_KEY));
1397 }
1398
1399 #[rstest]
1400 fn test_form_prefix() {
1401 let mut form = Form::with_prefix("profile".to_string());
1402 assert_eq!(form.prefix(), "profile");
1403 assert_eq!(form.add_prefix_to_field_name("name"), "profile-name");
1404
1405 form.set_prefix("user".to_string());
1406 assert_eq!(form.prefix(), "user");
1407 assert_eq!(form.add_prefix_to_field_name("email"), "user-email");
1408 }
1409
1410 #[rstest]
1411 fn prefixed_forms_do_not_fallback_to_unprefixed_values() {
1412 let mut form = Form::with_prefix("profile".to_string());
1414 form.add_field(Box::new(CharField::new("name".to_string()).required()));
1415 form.bind(HashMap::from([(String::from("name"), json!("other-form"))]));
1416
1417 let valid = form.is_valid();
1419
1420 assert!(!valid);
1422 assert_eq!(form.errors().get("name"), Some(&vec!["name".to_string()]));
1423 }
1424
1425 #[rstest]
1426 fn prefixed_forms_expose_only_canonical_cleaned_values() {
1427 let mut form = Form::with_prefix("profile".to_string());
1429 form.add_field(Box::new(CharField::new("name".to_string()).required()));
1430 form.bind(HashMap::from([(
1431 String::from("profile-name"),
1432 json!("Ada"),
1433 )]));
1434
1435 let valid = form.is_valid();
1437
1438 assert!(valid);
1440 assert_eq!(
1441 form.cleaned_data(),
1442 &HashMap::from([(String::from("name"), json!("Ada"))])
1443 );
1444 }
1445
1446 #[rstest]
1447 fn has_changed_uses_cleaned_values_after_validation() {
1448 let mut form = Form::with_initial(HashMap::from([("age".to_string(), json!(1))]));
1450 form.add_field(Box::new(IntegerField::new("age".to_string())));
1451 form.bind(HashMap::from([("age".to_string(), json!("1"))]));
1452
1453 let valid = form.is_valid();
1455
1456 assert!(valid);
1458 assert_eq!(form.cleaned_data().get("age"), Some(&json!(1)));
1459 assert!(!form.has_changed());
1460 }
1461
1462 #[rstest]
1463 fn has_changed_keeps_cleaned_values_after_later_field_error() {
1464 let mut form = Form::with_initial(HashMap::from([("age".to_string(), json!(1))]));
1466 form.add_field(Box::new(IntegerField::new("age".to_string())));
1467 form.add_clean_function(|_| {
1468 Err(FormError::Field {
1469 field: "age".to_string(),
1470 error: FieldError::validation(None, "Age is not allowed."),
1471 })
1472 });
1473 form.bind(HashMap::from([("age".to_string(), json!("1"))]));
1474
1475 let valid = form.is_valid();
1477
1478 assert!(!valid);
1480 assert_eq!(form.cleaned_data().get("age"), Some(&json!(1)));
1481 assert_eq!(
1482 form.errors().get("age"),
1483 Some(&vec![String::from("Age is not allowed.")])
1484 );
1485 assert!(!form.has_changed());
1486 }
1487
1488 #[rstest]
1489 fn prefixed_forms_preserve_overlapping_canonical_cleaned_values() {
1490 let mut form = Form::with_prefix("profile".to_string());
1492 form.add_field(Box::new(
1493 CharField::new("profile-name".to_string()).required(),
1494 ));
1495 form.add_field(Box::new(CharField::new("name".to_string()).required()));
1496 form.bind(HashMap::from([
1497 ("profile-profile-name".to_string(), json!("Ada")),
1498 ("profile-name".to_string(), json!("Grace")),
1499 ]));
1500
1501 let valid = form.is_valid();
1503
1504 assert!(valid);
1506 assert_eq!(form.cleaned_data().get("profile-name"), Some(&json!("Ada")));
1507 assert_eq!(form.cleaned_data().get("name"), Some(&json!("Grace")));
1508 }
1509
1510 #[rstest]
1511 fn prefixed_forms_preserve_bound_values_after_validation_failure() {
1512 let mut form = Form::with_prefix("profile".to_string());
1514 form.add_field(Box::new(CharField::new("name".to_string()).required()));
1515 form.add_field(Box::new(CharField::new("email".to_string()).required()));
1516 let expected_name = json!("Ada");
1517 form.bind(HashMap::from([(
1518 String::from("profile-name"),
1519 expected_name.clone(),
1520 )]));
1521
1522 let first_valid = form.is_valid();
1524 let first_bound_value = form.get_bound_field("name").unwrap().value().cloned();
1525 let second_valid = form.is_valid();
1526 let second_bound_value = form.get_bound_field("name").unwrap().value().cloned();
1527
1528 assert!(!first_valid);
1530 assert!(!second_valid);
1531 assert_eq!(first_bound_value, Some(expected_name.clone()));
1532 assert_eq!(second_bound_value, Some(expected_name));
1533 }
1534
1535 #[test]
1536 fn test_form_field_clean_function() {
1537 let mut form = Form::new();
1538
1539 let mut name_field = CharField::new("name".to_string());
1540 name_field.required = true;
1541 form.add_field(Box::new(name_field));
1542
1543 form.add_field_clean_function("name", |value| {
1545 if let Some(s) = value.as_str() {
1546 Ok(serde_json::json!(s.to_uppercase()))
1547 } else {
1548 Err(FormError::Validation("Expected string".to_string()))
1549 }
1550 });
1551
1552 let mut data = HashMap::new();
1553 data.insert("name".to_string(), serde_json::json!("john doe"));
1554 form.bind(data);
1555
1556 assert!(form.is_valid());
1557 assert_eq!(
1558 form.cleaned_data().get("name").unwrap(),
1559 &serde_json::json!("JOHN DOE")
1560 );
1561 }
1562
1563 #[rstest]
1564 fn form_configuration_and_submission_edges_are_observable() {
1565 let mut form = Form::with_prefix("profile".to_string());
1567 form.add_min_length_validator("username", 3, "Username is too short.");
1568 form.add_max_length_validator("username", 24, "Username is too long.");
1569 form.add_pattern_validator("username", "^[a-z]+$", "Lowercase letters only.");
1570 form.add_min_value_validator("age", 18.0, "Adults only.");
1571 form.add_max_value_validator("age", 120.0, "Age is too large.");
1572 form.add_email_validator("email", "Enter a valid email.");
1573 form.add_url_validator("website", "Enter a valid URL.");
1574 form.add_fields_equal_validator(
1575 vec!["password".to_string(), "confirmation".to_string()],
1576 "Passwords do not match.",
1577 Some("confirmation".to_string()),
1578 );
1579 form.add_validator_rule(
1580 "username",
1581 "reserved_words",
1582 json!({"scope": "registration"}),
1583 "Username is reserved.",
1584 );
1585 form.add_date_range_validator("starts_at", "ends_at", None);
1586 form.add_numeric_range_validator("minimum", "maximum", None);
1587
1588 assert_eq!(
1590 serde_json::to_value(form.validation_rules()).unwrap(),
1591 json!([
1592 {
1593 "type": "min_length",
1594 "field_name": "username",
1595 "min": 3,
1596 "error_message": "Username is too short."
1597 },
1598 {
1599 "type": "max_length",
1600 "field_name": "username",
1601 "max": 24,
1602 "error_message": "Username is too long."
1603 },
1604 {
1605 "type": "pattern",
1606 "field_name": "username",
1607 "pattern": "^[a-z]+$",
1608 "error_message": "Lowercase letters only."
1609 },
1610 {
1611 "type": "min_value",
1612 "field_name": "age",
1613 "min": 18.0,
1614 "error_message": "Adults only."
1615 },
1616 {
1617 "type": "max_value",
1618 "field_name": "age",
1619 "max": 120.0,
1620 "error_message": "Age is too large."
1621 },
1622 {
1623 "type": "email",
1624 "field_name": "email",
1625 "error_message": "Enter a valid email."
1626 },
1627 {
1628 "type": "url",
1629 "field_name": "website",
1630 "error_message": "Enter a valid URL."
1631 },
1632 {
1633 "type": "fields_equal",
1634 "field_names": ["password", "confirmation"],
1635 "error_message": "Passwords do not match.",
1636 "target_field": "confirmation"
1637 },
1638 {
1639 "type": "validator_ref",
1640 "field_name": "username",
1641 "validator_id": "reserved_words",
1642 "params": {"scope": "registration"},
1643 "error_message": "Username is reserved."
1644 },
1645 {
1646 "type": "date_range",
1647 "start_field": "starts_at",
1648 "end_field": "ends_at",
1649 "error_message": "End date must be after or equal to start date",
1650 "target_field": "ends_at"
1651 },
1652 {
1653 "type": "numeric_range",
1654 "min_field": "minimum",
1655 "max_field": "maximum",
1656 "error_message": "Maximum value must be greater than or equal to minimum value",
1657 "target_field": "maximum"
1658 }
1659 ]),
1660 );
1661 assert_eq!(form.prefix(), "profile");
1662 assert_eq!(form.add_prefix_to_field_name("email"), "profile-email");
1663 form.set_prefix("account".to_string());
1664 assert_eq!(form.add_prefix_to_field_name("email"), "account-email");
1665 assert_eq!(
1666 form.render_css_media(&["/assets/<theme>&.css"]),
1667 "<link rel=\"stylesheet\" href=\"/assets/<theme>&.css\" />\n",
1668 );
1669 assert_eq!(
1670 form.render_js_media(&["/assets/\"main\".js"]),
1671 "<script src=\"/assets/"main".js\"></script>\n",
1672 );
1673
1674 let mut csrf_form = Form::new();
1675 csrf_form.set_csrf_token("expected-token".to_string());
1676 csrf_form.bind(HashMap::new());
1677 assert!(!csrf_form.is_valid());
1678 assert_eq!(
1679 csrf_form.errors().get(ALL_FIELDS_KEY),
1680 Some(&vec!["CSRF token missing or incorrect.".to_string()]),
1681 );
1682 csrf_form.bind(HashMap::from([(
1683 "csrfmiddlewaretoken".to_string(),
1684 json!("wrong-token"),
1685 )]));
1686 assert!(!csrf_form.is_valid());
1687 assert_eq!(
1688 csrf_form.errors().get(ALL_FIELDS_KEY),
1689 Some(&vec!["CSRF token missing or incorrect.".to_string()]),
1690 );
1691 csrf_form.bind(HashMap::from([(
1692 "csrfmiddlewaretoken".to_string(),
1693 json!("expected-token"),
1694 )]));
1695 assert!(csrf_form.is_valid());
1696 csrf_form.add_error(ALL_FIELDS_KEY, "Cross-field validation failed.");
1697 assert_eq!(
1698 csrf_form.errors().get(ALL_FIELDS_KEY),
1699 Some(&vec!["Cross-field validation failed.".to_string()]),
1700 );
1701 }
1702}