use crate::bound_field::BoundField;
use crate::field::{FieldError, FormField};
use crate::wasm_compat::ValidationRule;
use std::collections::{HashMap, HashSet};
use std::ops::Index;
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
let hash_a = Sha256::digest(a);
let hash_b = Sha256::digest(b);
hash_a.ct_eq(&hash_b).into()
}
#[derive(Debug, thiserror::Error)]
pub enum FormError {
#[error("Field error in {field}: {error}")]
Field {
field: String,
error: FieldError,
},
#[error("Validation error: {0}")]
Validation(String),
#[error("No model instance available for save operation")]
NoInstance,
}
pub type FormResult<T> = Result<T, FormError>;
type CleanFunction =
Box<dyn Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync>;
type FieldCleanFunction =
Box<dyn Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync>;
pub const ALL_FIELDS_KEY: &str = "_all";
pub struct Form {
fields: Vec<Box<dyn FormField>>,
data: HashMap<String, serde_json::Value>,
cleaned_data: HashMap<String, serde_json::Value>,
cleaned_field_names: HashSet<String>,
initial: HashMap<String, serde_json::Value>,
errors: HashMap<String, Vec<String>>,
is_bound: bool,
validation_complete: bool,
clean_functions: Vec<CleanFunction>,
field_clean_functions: HashMap<String, FieldCleanFunction>,
prefix: String,
validation_rules: Vec<ValidationRule>,
csrf_token: Option<String>,
csrf_enabled: bool,
}
impl Form {
pub fn new() -> Self {
Self {
fields: vec![],
data: HashMap::new(),
cleaned_data: HashMap::new(),
cleaned_field_names: HashSet::new(),
initial: HashMap::new(),
errors: HashMap::new(),
is_bound: false,
validation_complete: false,
clean_functions: vec![],
field_clean_functions: HashMap::new(),
prefix: String::new(),
validation_rules: vec![],
csrf_token: None,
csrf_enabled: false,
}
}
pub fn with_initial(initial: HashMap<String, serde_json::Value>) -> Self {
Self {
fields: vec![],
data: HashMap::new(),
cleaned_data: HashMap::new(),
cleaned_field_names: HashSet::new(),
initial,
errors: HashMap::new(),
is_bound: false,
validation_complete: false,
clean_functions: vec![],
field_clean_functions: HashMap::new(),
prefix: String::new(),
validation_rules: vec![],
csrf_token: None,
csrf_enabled: false,
}
}
pub fn with_prefix(prefix: String) -> Self {
Self {
fields: vec![],
data: HashMap::new(),
cleaned_data: HashMap::new(),
cleaned_field_names: HashSet::new(),
initial: HashMap::new(),
errors: HashMap::new(),
is_bound: false,
validation_complete: false,
clean_functions: vec![],
field_clean_functions: HashMap::new(),
prefix,
validation_rules: vec![],
csrf_token: None,
csrf_enabled: false,
}
}
pub fn add_field(&mut self, field: Box<dyn FormField>) {
self.fields.push(field);
}
pub fn bind(&mut self, data: HashMap<String, serde_json::Value>) {
self.cleaned_data = data.clone();
self.cleaned_field_names.clear();
self.data = data;
self.is_bound = true;
self.validation_complete = false;
}
pub fn is_valid(&mut self) -> bool {
if !self.is_bound {
return false;
}
self.validation_complete = false;
self.errors.clear();
self.cleaned_data = self.data.clone();
self.cleaned_field_names.clear();
if !self.validate_csrf() {
self.errors
.entry(ALL_FIELDS_KEY.to_string())
.or_default()
.push("CSRF token missing or incorrect.".to_string());
return false;
}
for field in &self.fields {
let value = self.data_for_field(field.name());
match field.clean(value) {
Ok(mut cleaned) => {
if let Some(field_clean) = self.field_clean_functions.get(field.name()) {
match field_clean(&cleaned) {
Ok(further_cleaned) => {
cleaned = further_cleaned;
}
Err(e) => {
self.errors
.entry(field.name().to_string())
.or_default()
.push(e.to_string());
continue;
}
}
}
self.cleaned_field_names.insert(field.name().to_string());
let submitted_name = self.add_prefix_to_field_name(field.name());
if submitted_name != field.name()
&& !self.cleaned_field_names.contains(&submitted_name)
{
self.cleaned_data.remove(&submitted_name);
}
self.cleaned_data.insert(field.name().to_string(), cleaned);
}
Err(e) => {
self.errors
.entry(field.name().to_string())
.or_default()
.push(e.to_string());
}
}
}
for clean_fn in &self.clean_functions {
if let Err(e) = clean_fn(&self.cleaned_data) {
match e {
FormError::Field { field, error } => {
self.errors
.entry(field)
.or_default()
.push(error.to_string());
}
FormError::Validation(msg) => {
self.errors
.entry(ALL_FIELDS_KEY.to_string())
.or_default()
.push(msg);
}
FormError::NoInstance => {
self.errors
.entry(ALL_FIELDS_KEY.to_string())
.or_default()
.push(e.to_string());
}
}
}
}
self.validation_complete = true;
self.errors.is_empty()
}
pub fn cleaned_data(&self) -> &HashMap<String, serde_json::Value> {
&self.cleaned_data
}
pub fn errors(&self) -> &HashMap<String, Vec<String>> {
&self.errors
}
pub fn add_error(&mut self, field_name: impl Into<String>, message: impl Into<String>) {
self.errors
.entry(field_name.into())
.or_default()
.push(message.into());
}
pub fn is_bound(&self) -> bool {
self.is_bound
}
pub fn fields(&self) -> &[Box<dyn FormField>] {
&self.fields
}
pub fn initial(&self) -> &HashMap<String, serde_json::Value> {
&self.initial
}
pub fn set_initial(&mut self, initial: HashMap<String, serde_json::Value>) {
self.initial = initial;
}
pub fn has_changed(&self) -> bool {
if !self.is_bound {
return false;
}
for field in &self.fields {
let initial_val = self.initial.get(field.name());
let data_val = if self.validation_complete {
self.cleaned_data_for_field(field.name())
.or_else(|| self.data_for_field(field.name()))
} else {
self.data_for_field(field.name())
};
if field.has_changed(initial_val, data_val) {
return true;
}
}
false
}
pub fn get_field(&self, name: &str) -> Option<&dyn FormField> {
self.fields
.iter()
.find(|f| f.name() == name)
.map(|f| f.as_ref())
}
pub fn remove_field(&mut self, name: &str) -> Option<Box<dyn FormField>> {
let pos = self.fields.iter().position(|f| f.name() == name)?;
Some(self.fields.remove(pos))
}
pub fn field_count(&self) -> usize {
self.fields.len()
}
pub fn add_clean_function<F>(&mut self, f: F)
where
F: Fn(&HashMap<String, serde_json::Value>) -> FormResult<()> + Send + Sync + 'static,
{
self.clean_functions.push(Box::new(f));
}
pub fn add_field_clean_function<F>(&mut self, field_name: &str, f: F)
where
F: Fn(&serde_json::Value) -> FormResult<serde_json::Value> + Send + Sync + 'static,
{
self.field_clean_functions
.insert(field_name.to_string(), Box::new(f));
}
pub fn validation_rules(&self) -> &[ValidationRule] {
&self.validation_rules
}
pub fn add_min_length_validator(
&mut self,
field_name: impl Into<String>,
min: usize,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::MinLength {
field_name: field_name.into(),
min,
error_message: error_message.into(),
});
}
pub fn add_max_length_validator(
&mut self,
field_name: impl Into<String>,
max: usize,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::MaxLength {
field_name: field_name.into(),
max,
error_message: error_message.into(),
});
}
pub fn add_pattern_validator(
&mut self,
field_name: impl Into<String>,
pattern: impl Into<String>,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::Pattern {
field_name: field_name.into(),
pattern: pattern.into(),
error_message: error_message.into(),
});
}
pub fn add_min_value_validator(
&mut self,
field_name: impl Into<String>,
min: f64,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::MinValue {
field_name: field_name.into(),
min,
error_message: error_message.into(),
});
}
pub fn add_max_value_validator(
&mut self,
field_name: impl Into<String>,
max: f64,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::MaxValue {
field_name: field_name.into(),
max,
error_message: error_message.into(),
});
}
pub fn add_email_validator(
&mut self,
field_name: impl Into<String>,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::Email {
field_name: field_name.into(),
error_message: error_message.into(),
});
}
pub fn add_url_validator(
&mut self,
field_name: impl Into<String>,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::Url {
field_name: field_name.into(),
error_message: error_message.into(),
});
}
pub fn add_fields_equal_validator(
&mut self,
field_names: Vec<String>,
error_message: impl Into<String>,
target_field: Option<String>,
) {
self.validation_rules.push(ValidationRule::FieldsEqual {
field_names,
error_message: error_message.into(),
target_field,
});
}
pub fn add_validator_rule(
&mut self,
field_name: impl Into<String>,
validator_id: impl Into<String>,
params: serde_json::Value,
error_message: impl Into<String>,
) {
self.validation_rules.push(ValidationRule::ValidatorRef {
field_name: field_name.into(),
validator_id: validator_id.into(),
params,
error_message: error_message.into(),
});
}
pub fn add_date_range_validator(
&mut self,
start_field: impl Into<String>,
end_field: impl Into<String>,
error_message: Option<String>,
) {
let start = start_field.into();
let end = end_field.into();
let message = error_message
.unwrap_or_else(|| "End date must be after or equal to start date".to_string());
self.validation_rules.push(ValidationRule::DateRange {
start_field: start,
end_field: end.clone(),
error_message: message,
target_field: Some(end),
});
}
pub fn add_numeric_range_validator(
&mut self,
min_field: impl Into<String>,
max_field: impl Into<String>,
error_message: Option<String>,
) {
let min = min_field.into();
let max = max_field.into();
let message = error_message.unwrap_or_else(|| {
"Maximum value must be greater than or equal to minimum value".to_string()
});
self.validation_rules.push(ValidationRule::NumericRange {
min_field: min,
max_field: max.clone(),
error_message: message,
target_field: Some(max),
});
}
pub fn set_csrf_token(&mut self, token: String) {
self.csrf_token = Some(token);
self.csrf_enabled = true;
}
pub fn csrf_enabled(&self) -> bool {
self.csrf_enabled
}
pub fn csrf_token(&self) -> Option<&str> {
self.csrf_token.as_deref()
}
fn validate_csrf(&self) -> bool {
if !self.csrf_enabled {
return true;
}
let expected = match &self.csrf_token {
Some(t) => t,
None => return false,
};
let submitted = self
.data
.get("csrfmiddlewaretoken")
.and_then(|v| v.as_str());
match submitted {
Some(token) => {
constant_time_eq(token.as_bytes(), expected.as_bytes())
}
None => false,
}
}
pub fn prefix(&self) -> &str {
&self.prefix
}
pub fn set_prefix(&mut self, prefix: String) {
self.prefix = prefix;
}
pub fn add_prefix_to_field_name(&self, field_name: &str) -> String {
if self.prefix.is_empty() {
field_name.to_string()
} else {
format!("{}-{}", self.prefix, field_name)
}
}
fn data_for_field(&self, field_name: &str) -> Option<&serde_json::Value> {
if self.prefix.is_empty() {
self.data.get(field_name)
} else {
let prefixed_name = self.add_prefix_to_field_name(field_name);
self.data.get(&prefixed_name)
}
}
fn cleaned_data_for_field(&self, field_name: &str) -> Option<&serde_json::Value> {
if !self.cleaned_field_names.contains(field_name) {
return None;
}
self.cleaned_data.get(field_name)
}
pub fn render_css_media(&self, css_files: &[&str]) -> String {
use crate::field::escape_attribute;
let mut html = String::new();
for path in css_files {
html.push_str(&format!(
"<link rel=\"stylesheet\" href=\"{}\" />\n",
escape_attribute(path)
));
}
html
}
pub fn render_js_media(&self, js_files: &[&str]) -> String {
use crate::field::escape_attribute;
let mut html = String::new();
for path in js_files {
html.push_str(&format!(
"<script src=\"{}\"></script>\n",
escape_attribute(path)
));
}
html
}
pub fn get_bound_field<'a>(&'a self, name: &str) -> Option<BoundField<'a>> {
let field = self.get_field(name)?;
let data = if field.is_sensitive()
&& self.validation_complete
&& self.cleaned_field_names.contains(field.name())
{
self.cleaned_data_for_field(name)
} else {
self.data_for_field(name)
};
let errors = self.errors.get(name).map(|e| e.as_slice()).unwrap_or(&[]);
Some(BoundField::new(
"form".to_string(),
field,
data,
errors,
&self.prefix,
))
}
}
impl Default for Form {
fn default() -> Self {
Self::new()
}
}
impl Form {
#[allow(clippy::borrowed_box)]
pub fn get(&self, name: &str) -> Option<&Box<dyn FormField>> {
self.fields.iter().find(|f| f.name() == name)
}
}
impl Index<&str> for Form {
type Output = Box<dyn FormField>;
fn index(&self, name: &str) -> &Self::Output {
self.get(name)
.unwrap_or_else(|| panic!("Field '{}' not found", name))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fields::{CharField, IntegerField};
use rstest::rstest;
use serde_json::json;
#[test]
fn test_form_validation() {
let mut form = Form::new();
let mut name_field = CharField::new("name".to_string());
name_field.max_length = Some(50);
form.add_field(Box::new(name_field));
let mut data = HashMap::new();
data.insert("name".to_string(), serde_json::json!("John Doe"));
form.bind(data);
assert!(form.is_valid());
assert!(form.errors().is_empty());
}
#[test]
fn test_form_validation_error() {
let mut form = Form::new();
let mut name_field = CharField::new("name".to_string());
name_field.max_length = Some(5);
form.add_field(Box::new(name_field));
let mut data = HashMap::new();
data.insert("name".to_string(), serde_json::json!("Very Long Name"));
form.bind(data);
assert!(!form.is_valid());
assert!(!form.errors().is_empty());
}
#[test]
fn test_form_basic() {
use crate::fields::CharField;
let mut form = Form::new();
form.add_field(Box::new(CharField::new("first_name".to_string())));
form.add_field(Box::new(CharField::new("last_name".to_string())));
let mut data = HashMap::new();
data.insert("first_name".to_string(), serde_json::json!("John"));
data.insert("last_name".to_string(), serde_json::json!("Lennon"));
form.bind(data);
assert!(form.is_bound());
assert!(form.is_valid());
assert!(form.errors().is_empty());
let cleaned = form.cleaned_data();
assert_eq!(
cleaned.get("first_name").unwrap(),
&serde_json::json!("John")
);
assert_eq!(
cleaned.get("last_name").unwrap(),
&serde_json::json!("Lennon")
);
}
#[test]
fn test_form_missing_required_fields() {
use crate::fields::CharField;
let mut form = Form::new();
form.add_field(Box::new(CharField::new("username".to_string()).required()));
form.add_field(Box::new(CharField::new("email".to_string()).required()));
let data = HashMap::new();
form.bind(data);
assert!(form.is_bound());
assert!(!form.is_valid());
assert!(form.errors().contains_key("username"));
assert!(form.errors().contains_key("email"));
}
#[test]
fn test_form_optional_fields() {
use crate::fields::CharField;
let mut form = Form::new();
let username_field = CharField::new("username".to_string());
form.add_field(Box::new(username_field));
let mut bio_field = CharField::new("bio".to_string());
bio_field.required = false;
form.add_field(Box::new(bio_field));
let mut data = HashMap::new();
data.insert("username".to_string(), serde_json::json!("john"));
form.bind(data);
assert!(form.is_bound());
assert!(form.is_valid());
assert!(form.errors().is_empty());
}
#[test]
fn test_form_unbound() {
use crate::fields::CharField;
let mut form = Form::new();
form.add_field(Box::new(CharField::new("name".to_string())));
assert!(!form.is_bound());
assert!(!form.is_valid()); }
#[test]
fn test_form_extra_data() {
use crate::fields::CharField;
let mut form = Form::new();
form.add_field(Box::new(CharField::new("name".to_string())));
let mut data = HashMap::new();
data.insert("name".to_string(), serde_json::json!("John"));
data.insert(
"extra_field".to_string(),
serde_json::json!("should be ignored"),
);
form.bind(data);
assert!(form.is_valid());
let cleaned = form.cleaned_data();
assert_eq!(cleaned.get("name").unwrap(), &serde_json::json!("John"));
assert!(cleaned.contains_key("extra_field"));
}
#[test]
fn test_forms_form_multiple_fields() {
use crate::fields::{CharField, IntegerField};
let mut form = Form::new();
form.add_field(Box::new(CharField::new("username".to_string())));
let mut age_field = IntegerField::new("age".to_string());
age_field.min_value = Some(0);
age_field.max_value = Some(150);
form.add_field(Box::new(age_field));
let mut data = HashMap::new();
data.insert("username".to_string(), serde_json::json!("alice"));
data.insert("age".to_string(), serde_json::json!(30));
form.bind(data);
assert!(form.is_valid());
assert!(form.errors().is_empty());
}
#[test]
fn test_form_multiple_fields_invalid() {
use crate::fields::{CharField, IntegerField};
let mut form = Form::new();
let mut username_field = CharField::new("username".to_string());
username_field.min_length = Some(3);
form.add_field(Box::new(username_field));
let mut age_field = IntegerField::new("age".to_string());
age_field.min_value = Some(0);
age_field.max_value = Some(150);
form.add_field(Box::new(age_field));
let mut data = HashMap::new();
data.insert("username".to_string(), serde_json::json!("ab")); data.insert("age".to_string(), serde_json::json!(200));
form.bind(data);
assert!(!form.is_valid());
assert!(form.errors().contains_key("username"));
assert!(form.errors().contains_key("age"));
}
#[test]
fn test_form_multiple_instances() {
use crate::fields::CharField;
let mut form1 = Form::new();
form1.add_field(Box::new(CharField::new("name".to_string())));
let mut form2 = Form::new();
form2.add_field(Box::new(CharField::new("name".to_string())));
let mut data1 = HashMap::new();
data1.insert("name".to_string(), serde_json::json!("Form1"));
form1.bind(data1);
let mut data2 = HashMap::new();
data2.insert("name".to_string(), serde_json::json!("Form2"));
form2.bind(data2);
assert!(form1.is_valid());
assert!(form2.is_valid());
assert_eq!(
form1.cleaned_data().get("name").unwrap(),
&serde_json::json!("Form1")
);
assert_eq!(
form2.cleaned_data().get("name").unwrap(),
&serde_json::json!("Form2")
);
}
#[test]
fn test_form_with_initial_data() {
let mut initial = HashMap::new();
initial.insert("name".to_string(), serde_json::json!("Initial Name"));
initial.insert("age".to_string(), serde_json::json!(25));
let mut form = Form::with_initial(initial);
let name_field = CharField::new("name".to_string());
form.add_field(Box::new(name_field));
let age_field = crate::IntegerField::new("age".to_string());
form.add_field(Box::new(age_field));
assert_eq!(
form.initial().get("name").unwrap(),
&serde_json::json!("Initial Name")
);
assert_eq!(form.initial().get("age").unwrap(), &serde_json::json!(25));
}
#[test]
fn test_form_has_changed() {
let mut initial = HashMap::new();
initial.insert("name".to_string(), serde_json::json!("John"));
let mut form = Form::with_initial(initial);
let name_field = CharField::new("name".to_string());
form.add_field(Box::new(name_field));
let mut data1 = HashMap::new();
data1.insert("name".to_string(), serde_json::json!("John"));
form.bind(data1);
assert!(!form.has_changed());
let mut data2 = HashMap::new();
data2.insert("name".to_string(), serde_json::json!("Jane"));
form.bind(data2);
assert!(form.has_changed());
}
#[test]
fn test_form_index_access() {
let mut form = Form::new();
let name_field = CharField::new("name".to_string());
form.add_field(Box::new(name_field));
let field = &form["name"];
assert_eq!(field.name(), "name");
}
#[test]
#[should_panic(expected = "Field 'nonexistent' not found")]
fn test_form_index_access_nonexistent() {
let form = Form::new();
let _ = &form["nonexistent"];
}
#[test]
fn test_form_get_field() {
let mut form = Form::new();
let name_field = CharField::new("name".to_string());
form.add_field(Box::new(name_field));
assert!(form.get_field("name").is_some());
assert!(form.get_field("nonexistent").is_none());
}
#[test]
fn test_form_remove_field() {
let mut form = Form::new();
let name_field = CharField::new("name".to_string());
form.add_field(Box::new(name_field));
assert_eq!(form.field_count(), 1);
let removed = form.remove_field("name");
assert!(removed.is_some());
assert_eq!(form.field_count(), 0);
let not_removed = form.remove_field("nonexistent");
assert!(not_removed.is_none());
}
#[test]
fn test_form_custom_validation() {
let mut form = Form::new();
let mut password_field = CharField::new("password".to_string());
password_field.min_length = Some(8);
form.add_field(Box::new(password_field));
let mut confirm_field = CharField::new("confirm".to_string());
confirm_field.min_length = Some(8);
form.add_field(Box::new(confirm_field));
form.add_clean_function(|data| {
let password = data.get("password").and_then(|v| v.as_str());
let confirm = data.get("confirm").and_then(|v| v.as_str());
if password != confirm {
return Err(FormError::Validation("Passwords do not match".to_string()));
}
Ok(())
});
let mut data1 = HashMap::new();
data1.insert("password".to_string(), serde_json::json!("secret123"));
data1.insert("confirm".to_string(), serde_json::json!("secret123"));
form.bind(data1);
assert!(form.is_valid());
let mut data2 = HashMap::new();
data2.insert("password".to_string(), serde_json::json!("secret123"));
data2.insert("confirm".to_string(), serde_json::json!("different"));
form.bind(data2);
assert!(!form.is_valid());
assert!(form.errors().contains_key(ALL_FIELDS_KEY));
}
#[rstest]
fn test_form_prefix() {
let mut form = Form::with_prefix("profile".to_string());
assert_eq!(form.prefix(), "profile");
assert_eq!(form.add_prefix_to_field_name("name"), "profile-name");
form.set_prefix("user".to_string());
assert_eq!(form.prefix(), "user");
assert_eq!(form.add_prefix_to_field_name("email"), "user-email");
}
#[rstest]
fn prefixed_forms_do_not_fallback_to_unprefixed_values() {
let mut form = Form::with_prefix("profile".to_string());
form.add_field(Box::new(CharField::new("name".to_string()).required()));
form.bind(HashMap::from([(String::from("name"), json!("other-form"))]));
let valid = form.is_valid();
assert!(!valid);
assert_eq!(form.errors().get("name"), Some(&vec!["name".to_string()]));
}
#[rstest]
fn prefixed_forms_expose_only_canonical_cleaned_values() {
let mut form = Form::with_prefix("profile".to_string());
form.add_field(Box::new(CharField::new("name".to_string()).required()));
form.bind(HashMap::from([(
String::from("profile-name"),
json!("Ada"),
)]));
let valid = form.is_valid();
assert!(valid);
assert_eq!(
form.cleaned_data(),
&HashMap::from([(String::from("name"), json!("Ada"))])
);
}
#[rstest]
fn has_changed_uses_cleaned_values_after_validation() {
let mut form = Form::with_initial(HashMap::from([("age".to_string(), json!(1))]));
form.add_field(Box::new(IntegerField::new("age".to_string())));
form.bind(HashMap::from([("age".to_string(), json!("1"))]));
let valid = form.is_valid();
assert!(valid);
assert_eq!(form.cleaned_data().get("age"), Some(&json!(1)));
assert!(!form.has_changed());
}
#[rstest]
fn has_changed_keeps_cleaned_values_after_later_field_error() {
let mut form = Form::with_initial(HashMap::from([("age".to_string(), json!(1))]));
form.add_field(Box::new(IntegerField::new("age".to_string())));
form.add_clean_function(|_| {
Err(FormError::Field {
field: "age".to_string(),
error: FieldError::validation(None, "Age is not allowed."),
})
});
form.bind(HashMap::from([("age".to_string(), json!("1"))]));
let valid = form.is_valid();
assert!(!valid);
assert_eq!(form.cleaned_data().get("age"), Some(&json!(1)));
assert_eq!(
form.errors().get("age"),
Some(&vec![String::from("Age is not allowed.")])
);
assert!(!form.has_changed());
}
#[rstest]
fn prefixed_forms_preserve_overlapping_canonical_cleaned_values() {
let mut form = Form::with_prefix("profile".to_string());
form.add_field(Box::new(
CharField::new("profile-name".to_string()).required(),
));
form.add_field(Box::new(CharField::new("name".to_string()).required()));
form.bind(HashMap::from([
("profile-profile-name".to_string(), json!("Ada")),
("profile-name".to_string(), json!("Grace")),
]));
let valid = form.is_valid();
assert!(valid);
assert_eq!(form.cleaned_data().get("profile-name"), Some(&json!("Ada")));
assert_eq!(form.cleaned_data().get("name"), Some(&json!("Grace")));
}
#[rstest]
fn prefixed_forms_preserve_bound_values_after_validation_failure() {
let mut form = Form::with_prefix("profile".to_string());
form.add_field(Box::new(CharField::new("name".to_string()).required()));
form.add_field(Box::new(CharField::new("email".to_string()).required()));
let expected_name = json!("Ada");
form.bind(HashMap::from([(
String::from("profile-name"),
expected_name.clone(),
)]));
let first_valid = form.is_valid();
let first_bound_value = form.get_bound_field("name").unwrap().value().cloned();
let second_valid = form.is_valid();
let second_bound_value = form.get_bound_field("name").unwrap().value().cloned();
assert!(!first_valid);
assert!(!second_valid);
assert_eq!(first_bound_value, Some(expected_name.clone()));
assert_eq!(second_bound_value, Some(expected_name));
}
#[test]
fn test_form_field_clean_function() {
let mut form = Form::new();
let mut name_field = CharField::new("name".to_string());
name_field.required = true;
form.add_field(Box::new(name_field));
form.add_field_clean_function("name", |value| {
if let Some(s) = value.as_str() {
Ok(serde_json::json!(s.to_uppercase()))
} else {
Err(FormError::Validation("Expected string".to_string()))
}
});
let mut data = HashMap::new();
data.insert("name".to_string(), serde_json::json!("john doe"));
form.bind(data);
assert!(form.is_valid());
assert_eq!(
form.cleaned_data().get("name").unwrap(),
&serde_json::json!("JOHN DOE")
);
}
#[rstest]
fn form_configuration_and_submission_edges_are_observable() {
let mut form = Form::with_prefix("profile".to_string());
form.add_min_length_validator("username", 3, "Username is too short.");
form.add_max_length_validator("username", 24, "Username is too long.");
form.add_pattern_validator("username", "^[a-z]+$", "Lowercase letters only.");
form.add_min_value_validator("age", 18.0, "Adults only.");
form.add_max_value_validator("age", 120.0, "Age is too large.");
form.add_email_validator("email", "Enter a valid email.");
form.add_url_validator("website", "Enter a valid URL.");
form.add_fields_equal_validator(
vec!["password".to_string(), "confirmation".to_string()],
"Passwords do not match.",
Some("confirmation".to_string()),
);
form.add_validator_rule(
"username",
"reserved_words",
json!({"scope": "registration"}),
"Username is reserved.",
);
form.add_date_range_validator("starts_at", "ends_at", None);
form.add_numeric_range_validator("minimum", "maximum", None);
assert_eq!(
serde_json::to_value(form.validation_rules()).unwrap(),
json!([
{
"type": "min_length",
"field_name": "username",
"min": 3,
"error_message": "Username is too short."
},
{
"type": "max_length",
"field_name": "username",
"max": 24,
"error_message": "Username is too long."
},
{
"type": "pattern",
"field_name": "username",
"pattern": "^[a-z]+$",
"error_message": "Lowercase letters only."
},
{
"type": "min_value",
"field_name": "age",
"min": 18.0,
"error_message": "Adults only."
},
{
"type": "max_value",
"field_name": "age",
"max": 120.0,
"error_message": "Age is too large."
},
{
"type": "email",
"field_name": "email",
"error_message": "Enter a valid email."
},
{
"type": "url",
"field_name": "website",
"error_message": "Enter a valid URL."
},
{
"type": "fields_equal",
"field_names": ["password", "confirmation"],
"error_message": "Passwords do not match.",
"target_field": "confirmation"
},
{
"type": "validator_ref",
"field_name": "username",
"validator_id": "reserved_words",
"params": {"scope": "registration"},
"error_message": "Username is reserved."
},
{
"type": "date_range",
"start_field": "starts_at",
"end_field": "ends_at",
"error_message": "End date must be after or equal to start date",
"target_field": "ends_at"
},
{
"type": "numeric_range",
"min_field": "minimum",
"max_field": "maximum",
"error_message": "Maximum value must be greater than or equal to minimum value",
"target_field": "maximum"
}
]),
);
assert_eq!(form.prefix(), "profile");
assert_eq!(form.add_prefix_to_field_name("email"), "profile-email");
form.set_prefix("account".to_string());
assert_eq!(form.add_prefix_to_field_name("email"), "account-email");
assert_eq!(
form.render_css_media(&["/assets/<theme>&.css"]),
"<link rel=\"stylesheet\" href=\"/assets/<theme>&.css\" />\n",
);
assert_eq!(
form.render_js_media(&["/assets/\"main\".js"]),
"<script src=\"/assets/"main".js\"></script>\n",
);
let mut csrf_form = Form::new();
csrf_form.set_csrf_token("expected-token".to_string());
csrf_form.bind(HashMap::new());
assert!(!csrf_form.is_valid());
assert_eq!(
csrf_form.errors().get(ALL_FIELDS_KEY),
Some(&vec!["CSRF token missing or incorrect.".to_string()]),
);
csrf_form.bind(HashMap::from([(
"csrfmiddlewaretoken".to_string(),
json!("wrong-token"),
)]));
assert!(!csrf_form.is_valid());
assert_eq!(
csrf_form.errors().get(ALL_FIELDS_KEY),
Some(&vec!["CSRF token missing or incorrect.".to_string()]),
);
csrf_form.bind(HashMap::from([(
"csrfmiddlewaretoken".to_string(),
json!("expected-token"),
)]));
assert!(csrf_form.is_valid());
csrf_form.add_error(ALL_FIELDS_KEY, "Cross-field validation failed.");
assert_eq!(
csrf_form.errors().get(ALL_FIELDS_KEY),
Some(&vec!["Cross-field validation failed.".to_string()]),
);
}
}