From 4a4e49870d80b32364758b54dc0895aa3eb36d98 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 18 Jun 2025 00:20:08 +0000 Subject: [PATCH 01/19] Add more form attributes --- cot/src/form/fields.rs | 301 ++++++++++++++++++++++++++++++++--- cot/src/form/fields/attrs.rs | 67 ++++++++ cot/src/html.rs | 16 ++ 3 files changed, 365 insertions(+), 19 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 920e19b10..32318ecfc 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -10,7 +10,7 @@ use std::num::{ }; use askama::filters::HtmlSafe; -pub use attrs::Step; +pub use attrs::{AutoCapitalize, AutoComplete, List, Step}; pub use chrono::{ DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, @@ -72,11 +72,19 @@ pub(crate) use impl_form_field; impl_form_field!(StringField, StringFieldOptions, "a string"); /// Custom options for a [`StringField`]. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone)] pub struct StringFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + pub min_length: Option, + pub size: Option, + pub autocapitalize: Option, + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub placeholder: Option, + pub readonly: Option, } impl Display for StringField { @@ -90,6 +98,38 @@ impl Display for StringField { if let Some(max_length) = self.custom_options.max_length { tag.attr("maxlength", max_length.to_string()); } + if let Some(min_length) = self.custom_options.min_length { + tag.attr("minlength", min_length.to_string()); + } + + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + if let Some(dirname) = &self.custom_options.dirname { + tag.attr("dirname", dirname); + } + + if let Some(list) = &self.custom_options.list { + let list_id = format!("__{}_datalist", self.id()); + tag.attr("list", &list_id); + + let data_list = HtmlTag::data_list(list.clone(), &list_id); + tag.push_tag(data_list); + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -142,11 +182,16 @@ impl AsFormField for LimitedString { impl_form_field!(PasswordField, PasswordFieldOptions, "a password"); /// Custom options for a [`PasswordField`]. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone)] pub struct PasswordFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + pub min_length: Option, + pub size: Option, + pub autocomplete: Option, + pub placeholder: Option, + pub readonly: Option, } impl Display for PasswordField { @@ -160,6 +205,26 @@ impl Display for PasswordField { if let Some(max_length) = self.custom_options.max_length { tag.attr("maxlength", max_length.to_string()); } + + if let Some(min_length) = self.custom_options.min_length { + tag.attr("minlength", min_length.to_string()); + } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + // we don't set the value attribute for password fields // to avoid leaking the password in the HTML @@ -217,7 +282,7 @@ impl AsFormField for PasswordHash { impl_form_field!(EmailField, EmailFieldOptions, "an email"); /// Custom options for [`EmailField`] -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone)] pub struct EmailFieldOptions { /// The maximum length of the field used to set the `maxlength` attribute /// in the HTML input element. @@ -225,6 +290,13 @@ pub struct EmailFieldOptions { /// The minimum length of the field used to set the `minlength` attribute /// in the HTML input element. pub min_length: Option, + pub size: Option, + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub multiple: Option, + pub placeholder: Option, + pub readonly: Option, } impl Display for EmailField { @@ -241,6 +313,39 @@ impl Display for EmailField { if let Some(min_length) = self.custom_options.min_length { tag.attr("minlength", min_length.to_string()); } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + if let Some(dirname) = &self.custom_options.dirname { + tag.attr("dirname", dirname); + } + + if let Some(list) = &self.custom_options.list { + let list_id = format!("__{}_datalist", self.id()); + tag.attr("list", &list_id); + + let data_list = HtmlTag::data_list(list.clone(), &list_id); + tag.push_tag(data_list); + } + if let Some(multiple) = self.custom_options.multiple { + if multiple { + tag.bool_attr("multiple"); + } + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -289,10 +394,10 @@ impl AsFormField for Email { impl HtmlSafe for EmailField {} -impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer); +impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer + Display); /// Custom options for a [`IntegerField`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct IntegerFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -300,6 +405,12 @@ pub struct IntegerFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, + + pub placeholder: Option, + + pub readonly: Option, + + pub step: Option>, } impl Default for IntegerFieldOptions { @@ -307,11 +418,14 @@ impl Default for IntegerFieldOptions { Self { min: T::MIN, max: T::MAX, + placeholder: None, + readonly: None, + step: None, } } } -impl Display for IntegerField { +impl Display for IntegerField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut tag = HtmlTag::input("number"); tag.attr("name", self.id()); @@ -325,6 +439,19 @@ impl Display for IntegerField { if let Some(max) = &self.custom_options.max { tag.attr("max", max.to_string()); } + + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly{ + tag.bool_attr("readonly"); + } + } + if let Some(step) = &self.custom_options.step { + tag.attr("step", step.to_string()); + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -333,7 +460,7 @@ impl Display for IntegerField { } } -impl HtmlSafe for IntegerField {} +impl HtmlSafe for IntegerField {} /// A trait for numerical types that optionally have minimum and maximum values. /// @@ -646,10 +773,10 @@ pub(crate) fn check_required(field: &T) -> Result<&str, FormFieldV } } -impl_form_field!(FloatField, FloatFieldOptions, "a float", T: Float); +impl_form_field!(FloatField, FloatFieldOptions, "a float", T: Float + Display); /// Custom options for a [`FloatField`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct FloatFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -657,6 +784,12 @@ pub struct FloatFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, + + pub placeholder: Option, + + pub readonly: Option, + + pub step: Option>, } impl Default for FloatFieldOptions { @@ -664,11 +797,14 @@ impl Default for FloatFieldOptions { Self { min: T::MIN, max: T::MAX, + placeholder: None, + readonly: None, + step: None, } } } -impl Display for FloatField { +impl Display for FloatField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut tag: HtmlTag = HtmlTag::input("number"); tag.attr("name", self.id()); @@ -683,6 +819,17 @@ impl Display for FloatField { if let Some(max) = &self.custom_options.max { tag.attr("max", max.to_string()); } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly{ + tag.bool_attr("readonly"); + } + } + if let Some(step) = &self.custom_options.step { + tag.attr("step", step.to_string()); + } if let Some(value) = &self.value { tag.attr("value", value); } @@ -691,7 +838,7 @@ impl Display for FloatField { } } -impl HtmlSafe for FloatField {} +impl HtmlSafe for FloatField {} /// A trait for types that can be represented as a float. /// @@ -776,19 +923,59 @@ impl_float_as_form_field!(f64); impl_form_field!(UrlField, UrlFieldOptions, "a URL"); /// Custom options for a [`UrlField`]. -#[derive(Debug, Default, Copy, Clone)] -pub struct UrlFieldOptions; +#[derive(Debug, Default, Clone)] +pub struct UrlFieldOptions { + pub max_length: Option, + pub min_length: Option, + pub size: Option, + pub list: Option, + pub dirname: Option, + pub autocomplete: Option, + pub placeholder: Option, + pub readonly: Option, +} impl Display for UrlField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - // no custom options - let _ = self.custom_options; let mut tag = HtmlTag::input("url"); tag.attr("name", self.id()); tag.attr("id", self.id()); if self.options.required { tag.bool_attr("required"); } + if let Some(max_length) = self.custom_options.max_length { + tag.attr("maxlength", max_length.to_string()); + } + if let Some(min_length) = self.custom_options.min_length { + tag.attr("minlength", min_length.to_string()); + } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + if let Some(dirname) = &self.custom_options.dirname { + tag.attr("dirname", dirname); + } + + if let Some(list) = &self.custom_options.list { + let list_id = format!("__{}_datalist", self.id()); + tag.attr("list", &list_id); + + let data_list = HtmlTag::data_list(list.clone(), &list_id); + tag.push_tag(data_list); + } if let Some(value) = &self.value { tag.attr("value", value); } @@ -831,12 +1018,28 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocapitalize: Some(AutoCapitalize::Words), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dirname: Some("dir".to_string()), + list: Some(List::new(["bar", "baz"])), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), }, ); let html = field.to_string(); assert!(html.contains("type=\"text\"")); + assert!(html.contains("name=\"test\"")); + assert!(html.contains("id=\"test\"")); assert!(html.contains("required")); assert!(html.contains("maxlength=\"10\"")); + assert!(html.contains("minlength=\"5\"")); + assert!(html.contains("size=\"15\"")); + assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("dirname=\"dir\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("readonly")); } #[cot::test] @@ -849,6 +1052,7 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + ..Default::default() }, ); field @@ -869,6 +1073,7 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + ..Default::default() }, ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -903,6 +1108,7 @@ mod tests { }, PasswordFieldOptions { max_length: Some(10), + ..Default::default() }, ); field @@ -947,6 +1153,7 @@ mod tests { EmailFieldOptions { min_length: Some(10), max_length: Some(50), + ..Default::default() }, ); @@ -970,6 +1177,7 @@ mod tests { EmailFieldOptions { min_length: Some(10), max_length: Some(50), + ..Default::default() }, ); @@ -993,6 +1201,7 @@ mod tests { EmailFieldOptions { min_length: Some(5), max_length: Some(10), + ..Default::default() }, ); @@ -1019,6 +1228,7 @@ mod tests { EmailFieldOptions { min_length: Some(5), max_length: Some(10), + ..Default::default() }, ); @@ -1045,6 +1255,7 @@ mod tests { EmailFieldOptions { min_length: Some(50), max_length: Some(10), + ..Default::default() }, ); @@ -1092,6 +1303,7 @@ mod tests { IntegerFieldOptions { min: Some(1), max: Some(10), + ..Default::default() }, ); field @@ -1113,6 +1325,7 @@ mod tests { IntegerFieldOptions { min: Some(10), max: Some(50), + ..Default::default() }, ); field @@ -1137,6 +1350,7 @@ mod tests { IntegerFieldOptions { min: Some(10), max: Some(50), + ..Default::default() }, ); field @@ -1238,6 +1452,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1259,6 +1474,7 @@ mod tests { FloatFieldOptions { min: Some(5.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1283,6 +1499,7 @@ mod tests { FloatFieldOptions { min: Some(5.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1307,6 +1524,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); let bad_inputs = ["NaN", "inf"]; @@ -1337,6 +1555,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -1352,12 +1571,23 @@ mod tests { name: "test".to_owned(), required: true, }, - UrlFieldOptions, + UrlFieldOptions { + max_length: Some(100), + min_length: Some(5), + size: Some(30), + list: Some(List::new(["https://example.com"])), + dirname: Some("dir".to_owned()), + autocomplete: Some(AutoComplete::Value("url".to_owned())), + placeholder: Some("Enter URL".to_owned()), + readonly: Some(true), + }, ); + field .set_value(FormFieldValue::new_text("https://example.com")) .await .unwrap(); + let value = Url::clean_value(&field).unwrap(); assert_eq!( value.as_str(), @@ -1373,16 +1603,38 @@ mod tests { name: "url".to_owned(), required: true, }, - UrlFieldOptions, + UrlFieldOptions { + max_length: Some(120), + min_length: Some(10), + size: Some(40), + list: Some(List::new(["https://one.com", "https://two.com"])), + dirname: Some("lang".to_owned()), + autocomplete: Some(AutoComplete::Value("url".to_owned())), + placeholder: Some("Paste link".to_owned()), + readonly: Some(true), + }, ); + field .set_value(FormFieldValue::new_text("http://example.com")) .await .unwrap(); + let html = field.to_string(); + assert!(html.contains("type=\"url\"")); + assert!(html.contains("id=\"id_url\"")); + assert!(html.contains("name=\"id_url\"")); assert!(html.contains("required")); assert!(html.contains("value=\"http://example.com\"")); + assert!(html.contains("maxlength=\"120\"")); + assert!(html.contains("minlength=\"10\"")); + assert!(html.contains("size=\"40\"")); + assert!(html.contains("placeholder=\"Paste link\"")); + assert!(html.contains("autocomplete=\"url\"")); + assert!(html.contains("dirname=\"lang\"")); + assert!(html.contains("readonly")); + assert!(html.contains(" Display for Step { } } } + +#[derive(Debug, Clone, Default)] +pub struct List(Vec); + +impl List { + pub fn new(iter: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let v = iter.into_iter().map(|s| s.as_ref().to_string()).collect(); + Self(v) + } +} + +impl From for Vec { + fn from(value: List) -> Self { + value.0 + } +} + +#[derive(Debug, Clone)] +pub enum AutoComplete { + On, + Off, + Value(String), +} + +impl Display for AutoComplete { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Off => f.write_str("off"), + Self::On => f.write_str("on"), + Self::Value(value) => f.write_str(&value), + } + } +} +#[derive(Debug, Clone, Copy)] +pub enum AutoCapitalize { + Off, + On, + Words, + Characters, +} + +impl AutoCapitalize { + fn as_str(self) -> &'static str { + match self { + AutoCapitalize::Off => "off", + AutoCapitalize::On => "on", + AutoCapitalize::Words => "words", + AutoCapitalize::Characters => "characters", + } + } +} + +impl Display for AutoCapitalize { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Dir { + Rtl, + Ltr, +} diff --git a/cot/src/html.rs b/cot/src/html.rs index 000669119..c741783bb 100644 --- a/cot/src/html.rs +++ b/cot/src/html.rs @@ -189,6 +189,22 @@ impl HtmlTag { input } + pub fn data_list>>(list: L, id: &str) -> Self { + let mut data_list = Self::new("datalist"); + data_list.attr("id", id); + + let mut options: Vec = Vec::new(); + + for l in list.into() { + let mut option = HtmlTag::new("option"); + option.attr("value", l); + options.push(HtmlNode::Tag(option)); + } + + data_list.children = options; + data_list + } + /// Adds an attribute to the HTML tag. /// /// # Safety From 89708fa19955acf6b255cfc5f45db05c4de15792 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:22:56 +0000 Subject: [PATCH 02/19] chore(pre-commit.ci): auto fixes from pre-commit hooks --- cot/src/form/fields.rs | 4 ++-- cot/src/form/fields/attrs.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 32318ecfc..b21f1386d 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -444,7 +444,7 @@ impl Display for IntegerField { tag.attr("placeholder", placeholder); } if let Some(readonly) = self.custom_options.readonly { - if readonly{ + if readonly { tag.bool_attr("readonly"); } } @@ -823,7 +823,7 @@ impl Display for FloatField { tag.attr("placeholder", placeholder); } if let Some(readonly) = self.custom_options.readonly { - if readonly{ + if readonly { tag.bool_attr("readonly"); } } diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index c1805a2e2..0cd596d2b 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -74,9 +74,9 @@ pub enum AutoCapitalize { impl AutoCapitalize { fn as_str(self) -> &'static str { match self { - AutoCapitalize::Off => "off", - AutoCapitalize::On => "on", - AutoCapitalize::Words => "words", + AutoCapitalize::Off => "off", + AutoCapitalize::On => "on", + AutoCapitalize::Words => "words", AutoCapitalize::Characters => "characters", } } From 3a2b7aeafb5cf10d20ae603db9de54643a0834d4 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 18 Jun 2025 01:11:31 +0000 Subject: [PATCH 03/19] wrap input and datalist in a div --- cot/src/form/fields.rs | 13 +++++++++++-- cot/src/html.rs | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index b21f1386d..f048575ba 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -302,6 +302,8 @@ pub struct EmailFieldOptions { impl Display for EmailField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut tag = HtmlTag::input("email"); + let mut data_list: Option = None; + tag.attr("name", self.id()); tag.attr("id", self.id()); if self.options.required { @@ -337,8 +339,7 @@ impl Display for EmailField { let list_id = format!("__{}_datalist", self.id()); tag.attr("list", &list_id); - let data_list = HtmlTag::data_list(list.clone(), &list_id); - tag.push_tag(data_list); + data_list = Some(HtmlTag::data_list(list.clone(), &list_id)); } if let Some(multiple) = self.custom_options.multiple { if multiple { @@ -350,6 +351,14 @@ impl Display for EmailField { tag.attr("value", value); } + if let Some(data_list) = data_list { + let mut wrapper = HtmlTag::new("div"); + wrapper + .attr("id", format!("__{}_datalist_wrapper", self.id())) + .push_tag(tag) + .push_tag(data_list); + return write!(f, "{}", wrapper.render()); + } write!(f, "{}", tag.render()) } } diff --git a/cot/src/html.rs b/cot/src/html.rs index c741783bb..c77738a0c 100644 --- a/cot/src/html.rs +++ b/cot/src/html.rs @@ -197,7 +197,8 @@ impl HtmlTag { for l in list.into() { let mut option = HtmlTag::new("option"); - option.attr("value", l); + option.attr("value", &l); + option.push_str(&l); options.push(HtmlNode::Tag(option)); } From ae395e2440c9893203190f391ae8075eb07f382b Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 18 Jun 2025 19:01:23 +0000 Subject: [PATCH 04/19] remove multiple option --- cot/src/form/fields.rs | 6 ---- cot/src/form/fields/attrs.rs | 54 ++++++++++++++++++++++++++++++++---- cot/src/form/fields/files.rs | 22 +++++++++++++-- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index f048575ba..6d168e7ea 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -294,7 +294,6 @@ pub struct EmailFieldOptions { pub autocomplete: Option, pub dirname: Option, pub list: Option, - pub multiple: Option, pub placeholder: Option, pub readonly: Option, } @@ -341,11 +340,6 @@ impl Display for EmailField { data_list = Some(HtmlTag::data_list(list.clone(), &list_id)); } - if let Some(multiple) = self.custom_options.multiple { - if multiple { - tag.bool_attr("multiple"); - } - } if let Some(value) = &self.value { tag.attr("value", value); diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index 0cd596d2b..f5a6d4069 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -54,15 +54,20 @@ pub enum AutoComplete { Value(String), } -impl Display for AutoComplete { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl AutoComplete { + pub fn as_str(&self) -> &str { match self { - Self::Off => f.write_str("off"), - Self::On => f.write_str("on"), - Self::Value(value) => f.write_str(&value), + Self::Off => "off", + Self::On => "on", + Self::Value(value) => value.as_str(), } } } +impl Display for AutoComplete { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} #[derive(Debug, Clone, Copy)] pub enum AutoCapitalize { Off, @@ -93,3 +98,42 @@ pub enum Dir { Rtl, Ltr, } + +impl Dir { + pub fn as_str(&self) -> &'static str { + match self{ + Self::Rtl => "rtl", + Self::Ltr => "ltr" + } + } +} + +impl Display for Dir { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + + + +#[derive(Debug, Clone, Copy)] +pub enum Capture { + User, + Environment +} + + +impl Capture{ + pub fn as_str(&self) -> &'static str { + match self { + Self::User => "user", + Self::Environment => "environment" + } + } +} + +impl Display for Capture { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} \ No newline at end of file diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index fb1072803..01db6027c 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -6,6 +6,7 @@ use cot::form::{AsFormField, FormFieldValidationError}; use cot::html::HtmlTag; use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; +use crate::form::fields::attrs::Capture; #[derive(Debug)] /// A form field for a file. @@ -64,6 +65,8 @@ pub struct FileFieldOptions { /// /// [`accept` attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#limiting_accepted_file_types pub accept: Option>, + + pub capture: Option } impl Display for FileField { @@ -78,6 +81,10 @@ impl Display for FileField { tag.attr("accept", accept.join(",")); } + if let Some(capture) = self.custom_options.capture { + tag.attr("capture", capture.to_string()); + } + write!(f, "{}", tag.render()) } } @@ -161,6 +168,7 @@ mod tests { }, FileFieldOptions { accept: Some(vec!["image/*".to_string(), ".pdf".to_string()]), + capture: Some(Capture::Environment), }, ); @@ -169,6 +177,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(html.contains("accept=\"image/*,.pdf\"")); + assert!(html.contains("capture=\"environment\"")) } #[test] @@ -179,7 +188,10 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { + accept: None, + capture: Some(Capture::User), + }, ); let html = field.to_string(); @@ -187,6 +199,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(!html.contains("accept=")); + assert!(html.contains("capture=\"user\"")) } #[cot::test] @@ -197,7 +210,10 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { + ..Default::default() + + }, ); let boundary = "boundary"; @@ -232,7 +248,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { ..Default::default() }, ); let value = InMemoryUploadedFile::clean_value(&field); From e6b2f3dcad3dbf2cd04063c043f0c55eb99188f4 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 12 Jul 2025 18:20:21 +0000 Subject: [PATCH 05/19] rebase --- cot/src/form/fields.rs | 48 +++++++++++++++++++++++++++++++++--- cot/src/form/fields/attrs.rs | 15 +++++------ cot/src/form/fields/files.rs | 9 ++++--- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 6d168e7ea..688678774 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -1094,13 +1094,22 @@ mod tests { }, PasswordFieldOptions { max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + placeholder: Some("Enter password".to_string()), + readonly: Some(false), }, ); let html = field.to_string(); assert!(html.contains("type=\"password\"")); assert!(html.contains("required")); assert!(html.contains("maxlength=\"10\"")); + assert!(html.contains("minlength=\"5\"")); + assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("placeholder=\"Enter password\"")); } + #[cot::test] async fn password_field_clean_value() { let mut field = PasswordField::with_options( @@ -1131,18 +1140,31 @@ mod tests { required: true, }, EmailFieldOptions { - min_length: Some(10), - max_length: Some(50), + max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dirname: Some("dir".to_string()), + list: Some(List::new(["foo@example.com", "baz@example.com"])), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), }, ); let html = field.to_string(); + assert!(html.contains("type=\"email\"")); assert!(html.contains("required")); - assert!(html.contains("minlength=\"10\"")); - assert!(html.contains("maxlength=\"50\"")); + assert!(html.contains("minlength=\"5\"")); + assert!(html.contains("maxlength=\"10\"")); assert!(html.contains("name=\"test_id\"")); assert!(html.contains("id=\"test_id\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("readonly")); + assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("dirname=\"dir\"")); + assert!(html.contains("list=\"__test_id_datalist\"")); + assert!(html.contains(r#""#)); } #[cot::test] @@ -1286,13 +1308,22 @@ mod tests { IntegerFieldOptions { min: Some(1), max: Some(10), + placeholder: Some("Enter text".to_string()), + readonly: Some(false), + step: Some(Step::Value(10)), }, ); let html = field.to_string(); + assert!(html.contains("type=\"number\"")); assert!(html.contains("required")); assert!(html.contains("min=\"1\"")); assert!(html.contains("max=\"10\"")); + assert!(html.contains("step=\"10\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("name=\"test\"")); + assert!(html.contains("id=\"test\"")); + assert!(!html.contains("readonly")); } #[cot::test] @@ -1434,13 +1465,22 @@ mod tests { FloatFieldOptions { min: Some(1.5), max: Some(10.7), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), + step: Some(Step::Any), }, ); let html = field.to_string(); + assert!(html.contains("type=\"number\"")); assert!(html.contains("required")); assert!(html.contains("min=\"1.5\"")); assert!(html.contains("max=\"10.7\"")); + assert!(html.contains("step=\"any\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("name=\"test\"")); + assert!(html.contains("id=\"test\"")); + assert!(html.contains("readonly")); } #[cot::test] diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index f5a6d4069..8c2626ea5 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -101,9 +101,9 @@ pub enum Dir { impl Dir { pub fn as_str(&self) -> &'static str { - match self{ + match self { Self::Rtl => "rtl", - Self::Ltr => "ltr" + Self::Ltr => "ltr", } } } @@ -114,20 +114,17 @@ impl Display for Dir { } } - - #[derive(Debug, Clone, Copy)] pub enum Capture { User, - Environment + Environment, } - -impl Capture{ +impl Capture { pub fn as_str(&self) -> &'static str { match self { Self::User => "user", - Self::Environment => "environment" + Self::Environment => "environment", } } } @@ -136,4 +133,4 @@ impl Display for Capture { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) } -} \ No newline at end of file +} diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index 01db6027c..7a4905444 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -5,8 +5,8 @@ use bytes::Bytes; use cot::form::{AsFormField, FormFieldValidationError}; use cot::html::HtmlTag; -use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; use crate::form::fields::attrs::Capture; +use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; #[derive(Debug)] /// A form field for a file. @@ -66,7 +66,7 @@ pub struct FileFieldOptions { /// [`accept` attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#limiting_accepted_file_types pub accept: Option>, - pub capture: Option + pub capture: Option, } impl Display for FileField { @@ -212,7 +212,6 @@ mod tests { }, FileFieldOptions { ..Default::default() - }, ); @@ -248,7 +247,9 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { ..Default::default() }, + FileFieldOptions { + ..Default::default() + }, ); let value = InMemoryUploadedFile::clean_value(&field); From e0277972756fa5dc97016dd8d629eaf9ebb47082 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 12 Jul 2025 20:14:04 +0000 Subject: [PATCH 06/19] add more docs --- cot/src/form/fields.rs | 110 ++++++++++++++++++++++++++++++++--- cot/src/form/fields/attrs.rs | 48 ++++++++++++++- cot/src/form/fields/files.rs | 6 +- cot/src/html.rs | 10 ++++ 4 files changed, 161 insertions(+), 13 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 688678774..771ae1b5d 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -10,7 +10,7 @@ use std::num::{ }; use askama::filters::HtmlSafe; -pub use attrs::{AutoCapitalize, AutoComplete, List, Step}; +pub use attrs::{AutoCapitalize, AutoComplete, Dir, List, Step}; pub use chrono::{ DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, @@ -77,13 +77,33 @@ pub struct StringFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + /// The minimum length of the field. Used to set the `minlength` attribute + /// in the HTML input element. pub min_length: Option, + /// The size of the field. Used to set the `size` attribute in the HTML + /// input element. pub size: Option, + /// Corresponds to the [`AutoCapitalize`] attribute in the HTML input + /// element. pub autocapitalize: Option, + /// Corresponds to the [`AutoComplete`] attribute in the HTML input element. pub autocomplete: Option, + /// The direction of the text input, which can be set to `ltr` + /// (left-to-right) or `rtl` (right-to-left). This corresponds to the + /// [`Dir`] attribute in the HTML input element. + pub dir: Option, + /// The [`dirname`] attribute in the HTML input element, which is used to + /// specify the direction of the text input. + /// + /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname pub dirname: Option, + /// A [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. pub list: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. pub readonly: Option, } @@ -118,6 +138,10 @@ impl Display for StringField { tag.attr("size", size.to_string()); } + if let Some(dir) = &self.custom_options.dir { + tag.attr("dir", dir.as_str()); + } + if let Some(dirname) = &self.custom_options.dirname { tag.attr("dirname", dirname); } @@ -187,10 +211,20 @@ pub struct PasswordFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + /// The minimum length of the field. Used to set the `minlength` attribute + /// in the HTML input element. pub min_length: Option, + /// The size of the field. Used to set the [`size`] attribute in the HTML + /// input element. + /// + /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size pub size: Option, + /// Corresponds to the [`AutoComplete`] attribute in the HTML input element. pub autocomplete: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. pub readonly: Option, } @@ -290,11 +324,30 @@ pub struct EmailFieldOptions { /// The minimum length of the field used to set the `minlength` attribute /// in the HTML input element. pub min_length: Option, + /// The size of the field used to set the [`size`] attribute in the HTML + /// input element. + /// + /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size pub size: Option, + /// Corresponds to the [`AutoCapitalize`] attribute in the HTML input + /// element. pub autocomplete: Option, + /// The direction of the text input, which can be set to `ltr` + /// (left-to-right) or `rtl` (right-to-left). This corresponds to the + /// [`Dir`] attribute in the HTML input element. + pub dir: Option, + /// The [`dirname`] attribute in the HTML input element, which is used to + /// specify the direction of the text input. + /// + /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname pub dirname: Option, + /// A [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. pub list: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. pub readonly: Option, } @@ -330,6 +383,10 @@ impl Display for EmailField { tag.attr("size", size.to_string()); } + if let Some(dir) = &self.custom_options.dir { + tag.attr("dir", dir.as_str()); + } + if let Some(dirname) = &self.custom_options.dirname { tag.attr("dirname", dirname); } @@ -408,11 +465,13 @@ pub struct IntegerFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, - + /// The placeholder text for the input field, which is displayed when the + /// field is empty. pub placeholder: Option, - + /// If `true`, the field is read-only and cannot be modified by the user. pub readonly: Option, - + /// The step size for the field. Used to set the [`Step`] attribute in the + /// HTML input element. pub step: Option>, } @@ -787,11 +846,13 @@ pub struct FloatFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, - + /// The placeholder text for the input field, which is displayed when the + /// field is empty. pub placeholder: Option, - + /// If `true`, the field is read-only and cannot be modified by the user. pub readonly: Option, - + /// The step size for the field. Used to set the [`Step`] attribute in the + /// HTML input element. pub step: Option>, } @@ -928,13 +989,36 @@ impl_form_field!(UrlField, UrlFieldOptions, "a URL"); /// Custom options for a [`UrlField`]. #[derive(Debug, Default, Clone)] pub struct UrlFieldOptions { + /// The maximum length of the field. Used to set the `maxlength` attribute + /// in the HTML input element. pub max_length: Option, + /// The minimum length of the field. Used to set the `minlength` attribute + /// in the HTML input element. pub min_length: Option, + /// The size of the field. Used to set the [`size`]attribute in the HTML + /// input element. + /// + /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size pub size: Option, + /// The [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. pub list: Option, + /// The direction of the text input, which can be set to `ltr` + /// (left-to-right) or `rtl` (right-to-left). This corresponds to the + /// [`Dir`] attribute in the HTML input element. + pub dir: Option, + /// The [`dirname`] attribute in the HTML input element, which is used to + /// specify the direction of the text input. + /// + /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname pub dirname: Option, + /// The [`AutoComplete`] attribute in the HTML input element, which is used + /// to specify how the browser should handle autocomplete for the input. pub autocomplete: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. pub readonly: Option, } @@ -968,6 +1052,10 @@ impl Display for UrlField { tag.attr("size", size.to_string()); } + if let Some(dir) = &self.custom_options.dir { + tag.attr("dir", dir.as_str()); + } + if let Some(dirname) = &self.custom_options.dirname { tag.attr("dirname", dirname); } @@ -1025,6 +1113,7 @@ mod tests { size: Some(15), autocapitalize: Some(AutoCapitalize::Words), autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dir: Some(Dir::Ltr), dirname: Some("dir".to_string()), list: Some(List::new(["bar", "baz"])), placeholder: Some("Enter text".to_string()), @@ -1040,6 +1129,7 @@ mod tests { assert!(html.contains("minlength=\"5\"")); assert!(html.contains("size=\"15\"")); assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("dir=\"ltr\"")); assert!(html.contains("dirname=\"dir\"")); assert!(html.contains("placeholder=\"Enter text\"")); assert!(html.contains("readonly")); @@ -1144,6 +1234,7 @@ mod tests { min_length: Some(5), size: Some(15), autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dir: Some(Dir::Ltr), dirname: Some("dir".to_string()), list: Some(List::new(["foo@example.com", "baz@example.com"])), placeholder: Some("Enter text".to_string()), @@ -1162,6 +1253,7 @@ mod tests { assert!(html.contains("placeholder=\"Enter text\"")); assert!(html.contains("readonly")); assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("dir=\"ltr\"")); assert!(html.contains("dirname=\"dir\"")); assert!(html.contains("list=\"__test_id_datalist\"")); assert!(html.contains(r#""#)); @@ -1619,6 +1711,7 @@ mod tests { min_length: Some(5), size: Some(30), list: Some(List::new(["https://example.com"])), + dir: Some(Dir::Ltr), dirname: Some("dir".to_owned()), autocomplete: Some(AutoComplete::Value("url".to_owned())), placeholder: Some("Enter URL".to_owned()), @@ -1651,6 +1744,7 @@ mod tests { min_length: Some(10), size: Some(40), list: Some(List::new(["https://one.com", "https://two.com"])), + dir: Some(Dir::Ltr), dirname: Some("lang".to_owned()), autocomplete: Some(AutoComplete::Value("url".to_owned())), placeholder: Some("Paste link".to_owned()), @@ -1675,6 +1769,7 @@ mod tests { assert!(html.contains("size=\"40\"")); assert!(html.contains("placeholder=\"Paste link\"")); assert!(html.contains("autocomplete=\"url\"")); + assert!(html.contains("dir=\"ltr\"")); assert!(html.contains("dirname=\"lang\"")); assert!(html.contains("readonly")); assert!(html.contains("` elements: +/// Represents the HTML [`step`] attribute for `` elements: /// - `Any` → `step="any"` /// - `Value(T)` → `step=""` where `T` is converted appropriately +/// +/// [`step`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/step #[derive(Debug, Copy, Clone)] pub enum Step { /// Indicates that the user may enter any value (no fixed “step” interval). @@ -27,10 +29,15 @@ impl Display for Step { } } +/// Represents the HTML [`list`] attribute for `` elements. +/// Used to provide a set of predefined options for the input. +/// +/// [`list`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#list #[derive(Debug, Clone, Default)] pub struct List(Vec); impl List { + /// Creates a new `List` from any iterator of string-like items. pub fn new(iter: I) -> Self where I: IntoIterator, @@ -47,14 +54,22 @@ impl From for Vec { } } +/// Represents the HTML [`autocomplete`] attribute for form fields. +/// +/// [`autocomplete`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete #[derive(Debug, Clone)] pub enum AutoComplete { + /// Enables autocomplete. On, + /// Disables autocomplete. Off, + /// Custom autocomplete value. Value(String), } impl AutoComplete { + /// Returns the string representation for use in HTML. + #[must_use] pub fn as_str(&self) -> &str { match self { Self::Off => "off", @@ -63,20 +78,30 @@ impl AutoComplete { } } } + impl Display for AutoComplete { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) } } + +/// Represents the HTML [`autocapitalize`] attribute for form fields. +/// +/// [`autocapitalize`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autocapitalize #[derive(Debug, Clone, Copy)] pub enum AutoCapitalize { + /// No capitalization. Off, + /// Capitalize all letters. On, + /// Capitalize the first letter of each word. Words, + /// Capitalize all characters. Characters, } impl AutoCapitalize { + /// Returns the string representation for use in HTML. fn as_str(self) -> &'static str { match self { AutoCapitalize::Off => "off", @@ -93,17 +118,27 @@ impl Display for AutoCapitalize { } } +/// Represents the HTML [`dir`] attribute for text direction. +/// +/// [`dir`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir #[derive(Debug, Clone, Copy)] pub enum Dir { + /// Right-to-left text direction. Rtl, + /// Left-to-right text direction. Ltr, + /// User agent auto-detects the text direction. + Auto, } impl Dir { - pub fn as_str(&self) -> &'static str { + /// Returns the string representation for use in HTML. + #[must_use] + pub fn as_str(self) -> &'static str { match self { Self::Rtl => "rtl", Self::Ltr => "ltr", + Self::Auto => "auto", } } } @@ -114,14 +149,21 @@ impl Display for Dir { } } +/// Represents the HTML [`capture`] attribute for file inputs. +/// Used to specify the preferred source for file capture. +/// +/// [`capture`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#capture #[derive(Debug, Clone, Copy)] pub enum Capture { + /// Use the user-facing camera or microphone. User, + /// Use the environment-facing camera or microphone. Environment, } impl Capture { - pub fn as_str(&self) -> &'static str { + /// Returns the string representation for use in HTML. + pub fn as_str(self) -> &'static str { match self { Self::User => "user", Self::Environment => "environment", diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index 7a4905444..131080c4e 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -65,7 +65,7 @@ pub struct FileFieldOptions { /// /// [`accept` attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#limiting_accepted_file_types pub accept: Option>, - + /// The [`Capture`] attribute specifies the source of the file input. pub capture: Option, } @@ -177,7 +177,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(html.contains("accept=\"image/*,.pdf\"")); - assert!(html.contains("capture=\"environment\"")) + assert!(html.contains("capture=\"environment\"")); } #[test] @@ -199,7 +199,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(!html.contains("accept=")); - assert!(html.contains("capture=\"user\"")) + assert!(html.contains("capture=\"user\"")); } #[cot::test] diff --git a/cot/src/html.rs b/cot/src/html.rs index c77738a0c..a07ec51a1 100644 --- a/cot/src/html.rs +++ b/cot/src/html.rs @@ -189,6 +189,16 @@ impl HtmlTag { input } + /// Creates a new `HtmlTag` instance for a datalist element. + /// + /// # Examples + /// ``` + /// use cot::html::HtmlTag; + /// let data_list = HtmlTag::data_list(vec!["Option 1", "Option 2"], "my-datalist"); + /// let rendered = data_list.render(); + /// assert_eq!(rendered.as_str(), ""); + /// ``` + #[must_use] pub fn data_list>>(list: L, id: &str) -> Self { let mut data_list = Self::new("datalist"); data_list.attr("id", id); From 8b2122493636ea9600b7ab23dbe1a1280a845b61 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 8 Feb 2026 22:42:50 +0000 Subject: [PATCH 07/19] fix a number of issues --- cot-core/src/html.rs | 14 ++++++---- cot/src/form/fields.rs | 50 ++++++++++++++++++------------------ cot/src/form/fields/attrs.rs | 38 ++++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/cot-core/src/html.rs b/cot-core/src/html.rs index 37e46b8d3..7264d59dc 100644 --- a/cot-core/src/html.rs +++ b/cot-core/src/html.rs @@ -196,19 +196,23 @@ impl HtmlTag { /// use cot::html::HtmlTag; /// let data_list = HtmlTag::data_list(vec!["Option 1", "Option 2"], "my-datalist"); /// let rendered = data_list.render(); - /// assert_eq!(rendered.as_str(), ""); /// ``` #[must_use] - pub fn data_list>>(list: L, id: &str) -> Self { + pub fn data_list(list: I, id: &str) -> Self + where + I: IntoIterator, + S: AsRef, + { let mut data_list = Self::new("datalist"); data_list.attr("id", id); let mut options: Vec = Vec::new(); - for l in list.into() { + for item in list { + let l = item.as_ref(); let mut option = HtmlTag::new("option"); - option.attr("value", &l); - option.push_str(&l); + option.attr("value", l); + option.push_str(l); options.push(HtmlNode::Tag(option)); } diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index aeb69bd22..8375bdcb6 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -126,10 +126,10 @@ impl Display for StringField { if let Some(placeholder) = &self.custom_options.placeholder { tag.attr("placeholder", placeholder); } - if let Some(readonly) = self.custom_options.readonly { - if readonly { - tag.bool_attr("readonly"); - } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); } if let Some(autocomplete) = &self.custom_options.autocomplete { tag.attr("autocomplete", autocomplete.to_string()); @@ -247,10 +247,10 @@ impl Display for PasswordField { if let Some(placeholder) = &self.custom_options.placeholder { tag.attr("placeholder", placeholder); } - if let Some(readonly) = self.custom_options.readonly { - if readonly { - tag.bool_attr("readonly"); - } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); } if let Some(autocomplete) = &self.custom_options.autocomplete { tag.attr("autocomplete", autocomplete.to_string()); @@ -371,10 +371,10 @@ impl Display for EmailField { if let Some(placeholder) = &self.custom_options.placeholder { tag.attr("placeholder", placeholder); } - if let Some(readonly) = self.custom_options.readonly { - if readonly { - tag.bool_attr("readonly"); - } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); } if let Some(autocomplete) = &self.custom_options.autocomplete { tag.attr("autocomplete", autocomplete.to_string()); @@ -506,10 +506,10 @@ impl Display for IntegerField { if let Some(placeholder) = &self.custom_options.placeholder { tag.attr("placeholder", placeholder); } - if let Some(readonly) = self.custom_options.readonly { - if readonly { - tag.bool_attr("readonly"); - } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); } if let Some(step) = &self.custom_options.step { tag.attr("step", step.to_string()); @@ -887,10 +887,10 @@ impl Display for FloatField { if let Some(placeholder) = &self.custom_options.placeholder { tag.attr("placeholder", placeholder); } - if let Some(readonly) = self.custom_options.readonly { - if readonly { - tag.bool_attr("readonly"); - } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); } if let Some(step) = &self.custom_options.step { tag.attr("step", step.to_string()); @@ -1040,10 +1040,10 @@ impl Display for UrlField { if let Some(placeholder) = &self.custom_options.placeholder { tag.attr("placeholder", placeholder); } - if let Some(readonly) = self.custom_options.readonly { - if readonly { - tag.bool_attr("readonly"); - } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); } if let Some(autocomplete) = &self.custom_options.autocomplete { tag.attr("autocomplete", autocomplete.to_string()); @@ -1065,7 +1065,7 @@ impl Display for UrlField { let list_id = format!("__{}_datalist", self.id()); tag.attr("list", &list_id); - let data_list = HtmlTag::data_list(list.clone(), &list_id); + let data_list = HtmlTag::data_list(list, &list_id); tag.push_tag(data_list); } if let Some(value) = &self.value { diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index 9c1df7457..904fad15c 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -38,6 +38,12 @@ pub struct List(Vec); impl List { /// Creates a new `List` from any iterator of string-like items. + /// + /// # Examples + /// ``` + /// use cot::form::fields::List; + /// let list = List::new(vec!["Option 1", "Option 2", "Option 3"]); + /// ``` pub fn new(iter: I) -> Self where I: IntoIterator, @@ -46,11 +52,37 @@ impl List { let v = iter.into_iter().map(|s| s.as_ref().to_string()).collect(); Self(v) } + + /// Returns an iterator over the items in the list. + /// + /// # Examples + /// ``` + /// use cot::form::fields::List; + /// let list = List::new(vec!["Option 1", "Option 2", "Option 3"]); + /// for item in list.iter() { + /// println!("{item:?}"); + /// } + /// ``` + pub fn iter(&self) -> std::slice::Iter<'_, String> { + self.0.iter() + } } -impl From for Vec { - fn from(value: List) -> Self { - value.0 +impl IntoIterator for List { + type Item = String; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a List { + type Item = &'a String; + type IntoIter = std::slice::Iter<'a, String>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() } } From fe86f688a25e52394b301b1f03216acb5b9bc4e8 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 9 Feb 2026 11:44:08 +0000 Subject: [PATCH 08/19] increase cov --- cot/src/form/fields/attrs.rs | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index 904fad15c..b7410fab5 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -208,3 +208,53 @@ impl Display for Capture { f.write_str(self.as_str()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn autocapitalize_as_str() { + assert_eq!(AutoCapitalize::Off.as_str(), "off"); + assert_eq!(AutoCapitalize::On.as_str(), "on"); + assert_eq!(AutoCapitalize::Words.as_str(), "words"); + assert_eq!(AutoCapitalize::Characters.as_str(), "characters"); + } + + #[test] + fn autocapitalize_to_string() { + assert_eq!(AutoCapitalize::Off.to_string(), "off"); + assert_eq!(AutoCapitalize::On.to_string(), "on"); + assert_eq!(AutoCapitalize::Words.to_string(), "words"); + assert_eq!(AutoCapitalize::Characters.to_string(), "characters"); + } + + #[test] + fn autocomplete_as_str() { + assert_eq!(AutoComplete::Off.as_str(), "off"); + assert_eq!(AutoComplete::On.as_str(), "on"); + let custom = AutoComplete::Value("email".to_string()); + assert_eq!(custom.as_str(), "email"); + } + + #[test] + fn dir_as_str() { + assert_eq!(Dir::Rtl.as_str(), "rtl"); + assert_eq!(Dir::Ltr.as_str(), "ltr"); + assert_eq!(Dir::Auto.as_str(), "auto"); + } + + #[test] + fn dir_to_string() { + assert_eq!(Dir::Rtl.to_string(), "rtl"); + assert_eq!(Dir::Ltr.to_string(), "ltr"); + assert_eq!(Dir::Auto.to_string(), "auto"); + } + + #[test] + fn list_iter() { + let list = List::new(["Option 1", "Option 2", "Option 3"]); + let collected: Vec<&str> = list.iter().map(|s| s.as_str()).collect(); + assert_eq!(collected, vec!["Option 1", "Option 2", "Option 3"]); + } +} From 6eb7feef938246cfca66069f6181d7b0124aab0a Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 9 Feb 2026 12:57:45 +0000 Subject: [PATCH 09/19] clippy fix --- cot/src/form/fields/attrs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index b7410fab5..ac0962dd4 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -254,7 +254,7 @@ mod tests { #[test] fn list_iter() { let list = List::new(["Option 1", "Option 2", "Option 3"]); - let collected: Vec<&str> = list.iter().map(|s| s.as_str()).collect(); + let collected: Vec<&str> = list.iter().map(String::as_str).collect(); assert_eq!(collected, vec!["Option 1", "Option 2", "Option 3"]); } } From 65cddbee449e158c07d38bbf4665dd195754030f Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 16 Feb 2026 16:00:20 +0000 Subject: [PATCH 10/19] address nits --- cot-core/src/html.rs | 10 ++-- cot/src/form/fields.rs | 106 +++++++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/cot-core/src/html.rs b/cot-core/src/html.rs index 7264d59dc..9712ef5c9 100644 --- a/cot-core/src/html.rs +++ b/cot-core/src/html.rs @@ -172,7 +172,7 @@ impl HtmlTag { } } - /// Creates a new `HtmlTag` instance for an input element. + /// Creates a new `HtmlTag` instance for an [input] element. /// /// # Examples /// @@ -182,6 +182,8 @@ impl HtmlTag { /// let input = HtmlTag::input("text"); /// assert_eq!(input.render().as_str(), ""); /// ``` + /// + /// [input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input #[must_use] pub fn input(input_type: &str) -> Self { let mut input = Self::new("input"); @@ -189,7 +191,7 @@ impl HtmlTag { input } - /// Creates a new `HtmlTag` instance for a datalist element. + /// Creates a new `HtmlTag` instance for a [datalist] element. /// /// # Examples /// ``` @@ -197,6 +199,8 @@ impl HtmlTag { /// let data_list = HtmlTag::data_list(vec!["Option 1", "Option 2"], "my-datalist"); /// let rendered = data_list.render(); /// ``` + /// + /// [datalist]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/datalist #[must_use] pub fn data_list(list: I, id: &str) -> Self where @@ -212,7 +216,7 @@ impl HtmlTag { let l = item.as_ref(); let mut option = HtmlTag::new("option"); option.attr("value", l); - option.push_str(l); + // option.push_str(l); options.push(HtmlNode::Tag(option)); } diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 8375bdcb6..d43087ed4 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -123,22 +123,18 @@ impl Display for StringField { tag.attr("minlength", min_length.to_string()); } - if let Some(placeholder) = &self.custom_options.placeholder { - tag.attr("placeholder", placeholder); + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); } - if let Some(readonly) = self.custom_options.readonly - && readonly - { - tag.bool_attr("readonly"); + + if let Some(autocapitalize) = &self.custom_options.autocapitalize { + tag.attr("autocapitalize", autocapitalize.to_string()); } + if let Some(autocomplete) = &self.custom_options.autocomplete { tag.attr("autocomplete", autocomplete.to_string()); } - if let Some(size) = self.custom_options.size { - tag.attr("size", size.to_string()); - } - if let Some(dir) = &self.custom_options.dir { tag.attr("dir", dir.as_str()); } @@ -155,6 +151,16 @@ impl Display for StringField { tag.push_tag(data_list); } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -244,6 +250,15 @@ impl Display for PasswordField { if let Some(min_length) = self.custom_options.min_length { tag.attr("minlength", min_length.to_string()); } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + if let Some(placeholder) = &self.custom_options.placeholder { tag.attr("placeholder", placeholder); } @@ -252,13 +267,6 @@ impl Display for PasswordField { { tag.bool_attr("readonly"); } - if let Some(autocomplete) = &self.custom_options.autocomplete { - tag.attr("autocomplete", autocomplete.to_string()); - } - - if let Some(size) = self.custom_options.size { - tag.attr("size", size.to_string()); - } // we don't set the value attribute for password fields // to avoid leaking the password in the HTML @@ -368,22 +376,15 @@ impl Display for EmailField { if let Some(min_length) = self.custom_options.min_length { tag.attr("minlength", min_length.to_string()); } - if let Some(placeholder) = &self.custom_options.placeholder { - tag.attr("placeholder", placeholder); - } - if let Some(readonly) = self.custom_options.readonly - && readonly - { - tag.bool_attr("readonly"); - } - if let Some(autocomplete) = &self.custom_options.autocomplete { - tag.attr("autocomplete", autocomplete.to_string()); - } if let Some(size) = self.custom_options.size { tag.attr("size", size.to_string()); } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + if let Some(dir) = &self.custom_options.dir { tag.attr("dir", dir.as_str()); } @@ -399,6 +400,15 @@ impl Display for EmailField { data_list = Some(HtmlTag::data_list(list.clone(), &list_id)); } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -1001,9 +1011,9 @@ pub struct UrlFieldOptions { /// /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size pub size: Option, - /// The [`List`] of options for the `datalist` element, which can be used to - /// provide predefined options for the input. - pub list: Option, + /// The [`AutoComplete`] attribute in the HTML input element, which is used + /// to specify how the browser should handle autocomplete for the input. + pub autocomplete: Option, /// The direction of the text input, which can be set to `ltr` /// (left-to-right) or `rtl` (right-to-left). This corresponds to the /// [`Dir`] attribute in the HTML input element. @@ -1013,9 +1023,9 @@ pub struct UrlFieldOptions { /// /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname pub dirname: Option, - /// The [`AutoComplete`] attribute in the HTML input element, which is used - /// to specify how the browser should handle autocomplete for the input. - pub autocomplete: Option, + /// The [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. + pub list: Option, /// The placeholder text for the input field, which is displayed when the /// field is empty. pub placeholder: Option, @@ -1037,22 +1047,15 @@ impl Display for UrlField { if let Some(min_length) = self.custom_options.min_length { tag.attr("minlength", min_length.to_string()); } - if let Some(placeholder) = &self.custom_options.placeholder { - tag.attr("placeholder", placeholder); - } - if let Some(readonly) = self.custom_options.readonly - && readonly - { - tag.bool_attr("readonly"); - } - if let Some(autocomplete) = &self.custom_options.autocomplete { - tag.attr("autocomplete", autocomplete.to_string()); - } if let Some(size) = self.custom_options.size { tag.attr("size", size.to_string()); } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + if let Some(dir) = &self.custom_options.dir { tag.attr("dir", dir.as_str()); } @@ -1068,6 +1071,16 @@ impl Display for UrlField { let data_list = HtmlTag::data_list(list, &list_id); tag.push_tag(data_list); } + + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly + && readonly + { + tag.bool_attr("readonly"); + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -1134,6 +1147,7 @@ mod tests { assert!(html.contains("dirname=\"dir\"")); assert!(html.contains("placeholder=\"Enter text\"")); assert!(html.contains("readonly")); + assert!(html.contains("")); } #[cot::test] @@ -1257,7 +1271,7 @@ mod tests { assert!(html.contains("dir=\"ltr\"")); assert!(html.contains("dirname=\"dir\"")); assert!(html.contains("list=\"__test_id_datalist\"")); - assert!(html.contains(r#""#)); + assert!(html.contains(r#""#)); } #[cot::test] From c410d867ff8ce3cf8100d8a5a227aaa282b2284d Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 29 Jun 2026 23:56:47 +0000 Subject: [PATCH 11/19] add non exhaustive --- cot-core/src/html.rs | 1 - cot/src/form/fields/attrs.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cot-core/src/html.rs b/cot-core/src/html.rs index 923d84498..a73709408 100644 --- a/cot-core/src/html.rs +++ b/cot-core/src/html.rs @@ -235,7 +235,6 @@ impl HtmlTag { let l = item.as_ref(); let mut option = HtmlTag::new("option"); option.attr("value", l); - // option.push_str(l); options.push(HtmlNode::Tag(option)); } diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index ac0962dd4..453b8e4e4 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -90,6 +90,7 @@ impl<'a> IntoIterator for &'a List { /// /// [`autocomplete`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete #[derive(Debug, Clone)] +#[non_exhaustive] pub enum AutoComplete { /// Enables autocomplete. On, From b345b6137bbf3cd3f15db48fd43982ffea65a9c7 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 30 Jun 2026 00:08:52 +0000 Subject: [PATCH 12/19] add non exhaustive to Filefieldoptions --- cot/src/form/fields/files.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index 131080c4e..ebf9c57a5 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -53,6 +53,7 @@ impl FormField for FileField { /// Custom options for a [`FileField`]. #[derive(Debug, Default, Clone)] +#[non_exhaustive] pub struct FileFieldOptions { /// The accepted file types. Used to set the [`accept` attribute] in the /// HTML input element. Each string in the vector represents a file type From 8b5b1508d1ce1f3428acf94d70321ed598fb1853 Mon Sep 17 00:00:00 2001 From: Elijah Date: Tue, 7 Jul 2026 12:34:16 +0000 Subject: [PATCH 13/19] add non exhaustive to all FieldOptions --- cot/src/form/fields/attrs.rs | 4 ++++ cot/src/form/fields/chrono.rs | 4 ++++ cot/src/form/fields/select.rs | 2 ++ 3 files changed, 10 insertions(+) diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index 453b8e4e4..4a137de0c 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter}; /// /// [`step`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/step #[derive(Debug, Copy, Clone)] +#[non_exhaustive] pub enum Step { /// Indicates that the user may enter any value (no fixed “step” interval). /// @@ -122,6 +123,7 @@ impl Display for AutoComplete { /// /// [`autocapitalize`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autocapitalize #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub enum AutoCapitalize { /// No capitalization. Off, @@ -155,6 +157,7 @@ impl Display for AutoCapitalize { /// /// [`dir`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub enum Dir { /// Right-to-left text direction. Rtl, @@ -187,6 +190,7 @@ impl Display for Dir { /// /// [`capture`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#capture #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub enum Capture { /// Use the user-facing camera or microphone. User, diff --git a/cot/src/form/fields/chrono.rs b/cot/src/form/fields/chrono.rs index 7f1214e6e..3ffd9c8d7 100644 --- a/cot/src/form/fields/chrono.rs +++ b/cot/src/form/fields/chrono.rs @@ -143,6 +143,7 @@ impl_form_field!(DateTimeField, DateTimeFieldOptions, "a datetime"); /// ); /// ``` #[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] pub struct DateTimeFieldOptions { /// The maximum datetime value of the field used to set the `max` attribute /// in the HTML input element. @@ -291,6 +292,7 @@ impl From for FormFieldValidationError { /// ); /// ``` #[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] pub struct DateTimeWithTimezoneFieldOptions { /// The maximum allowed datetime (with offset) for this field. /// @@ -476,6 +478,7 @@ impl_form_field!(TimeField, TimeFieldOptions, "a time"); /// ); /// ``` #[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] pub struct TimeFieldOptions { /// The maximum time value of the field used to set the `max` attribute /// in the HTML input element. @@ -597,6 +600,7 @@ impl_form_field!(DateField, DateFieldOptions, "a date"); /// ); /// ``` #[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] pub struct DateFieldOptions { /// The maximum date value of the field used to set the `max` attribute /// in the HTML input element. diff --git a/cot/src/form/fields/select.rs b/cot/src/form/fields/select.rs index 831af0bcc..75140743d 100644 --- a/cot/src/form/fields/select.rs +++ b/cot/src/form/fields/select.rs @@ -184,6 +184,7 @@ impl_form_field!(SelectField, SelectFieldOptions, "a dropdown list", T: SelectCh /// Custom options for a [`SelectField`]. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct SelectFieldOptions { /// The list of available choices for the select field. /// If not set, the default choices from [`SelectChoice::default_choices`] @@ -282,6 +283,7 @@ impl FormField for SelectMultipleField { /// Custom options for a [`SelectMultipleField`]. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct SelectMultipleFieldOptions { /// The list of available choices for the multi-select field. /// If not set, the default choices from [`SelectChoice::default_choices`] From 45a008bbe2ec0a01bfccbc56ed315ac3e7ea70c2 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 8 Jul 2026 01:05:52 +0000 Subject: [PATCH 14/19] add builders to Options --- cot/src/form/fields.rs | 449 +++++++++++++++++++++------------- cot/src/form/fields/chrono.rs | 393 ++++++++++++++--------------- cot/src/form/fields/files.rs | 32 +-- cot/src/form/fields/select.rs | 31 +-- 4 files changed, 500 insertions(+), 405 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index d43087ed4..3de29a691 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -15,6 +15,7 @@ pub use chrono::{ DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, }; +use derive_builder::Builder; pub use files::{FileField, FileFieldOptions, InMemoryUploadedFile}; pub(crate) use select::check_required_multiple; pub use select::{ @@ -68,12 +69,95 @@ macro_rules! impl_form_field { } }; } + +macro_rules! impl_field_options_builder { + ( + $ty:ident < $($gen:ident),+ >, + $builder:ident < $($gen2:ident),+ > + { $($field:ident),+ $(,)? } + ) => { + impl<$($gen: Clone),+> $ty<$($gen),+> { + #[doc = concat!( + "Creates a new [`", stringify!($builder), "`] to build a ", + "[`", stringify!($ty), "`].\n\n", + "# Examples\n\n", + "```\n", + "# use cot::form::fields::", stringify!($ty), ";\n", + "let options = ", stringify!($ty), "::::builder().build();\n", + "```", + )] + #[must_use] + pub fn builder() -> $builder<$($gen),+> { + $builder::default() + } + } + + impl<$($gen2: Clone),+> $builder<$($gen2),+> { + #[doc = concat!( + "Builds the [`", stringify!($ty), "`], falling back to ", + "defaults for any field that wasn't explicitly set.\n\n", + "# Examples\n\n", + "```\n", + "# use cot::form::fields::", stringify!($ty), ";\n", + "let options = ", stringify!($ty), "::::builder().build();\n", + "```", + )] + #[must_use] + pub fn build(&self) -> $ty<$($gen2),+> { + $ty { + $( $field: self.$field.clone().unwrap_or_default(), )+ + } + } + } + }; + + ($ty:ident, $builder:ident { $($field:ident),+ $(,)? }) => { + impl $ty { + #[doc = concat!( + "Creates a new [`", stringify!($builder), "`] to build a ", + "[`", stringify!($ty), "`].\n\n", + "# Examples\n\n", + "```\n", + "# use cot::form::fields::", stringify!($ty), ";\n", + "let options = ", stringify!($ty), "::builder().build();\n", + "```", + )] + #[must_use] + pub fn builder() -> $builder { + $builder::default() + } + } + + impl $builder { + #[doc = concat!( + "Builds the [`", stringify!($ty), "`], falling back to ", + "defaults for any field that wasn't explicitly set.\n\n", + "# Examples\n\n", + "```\n", + "# use cot::form::fields::", stringify!($ty), ";\n", + "let options = ", stringify!($ty), "::builder().build();\n", + "```", + )] + #[must_use] + pub fn build(&self) -> $ty { + $ty { + $( $field: self.$field.clone().unwrap_or_default(), )+ + } + } + } + }; +} + +pub(crate) use impl_field_options_builder; pub(crate) use impl_form_field; impl_form_field!(StringField, StringFieldOptions, "a string"); /// Custom options for a [`StringField`]. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] +#[non_exhaustive] pub struct StringFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. @@ -213,7 +297,10 @@ impl AsFormField for LimitedString { impl_form_field!(PasswordField, PasswordFieldOptions, "a password"); /// Custom options for a [`PasswordField`]. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] +#[non_exhaustive] pub struct PasswordFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. @@ -325,7 +412,10 @@ impl AsFormField for PasswordHash { impl_form_field!(EmailField, EmailFieldOptions, "an email"); /// Custom options for [`EmailField`] -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] +#[non_exhaustive] pub struct EmailFieldOptions { /// The maximum length of the field used to set the `maxlength` attribute /// in the HTML input element. @@ -468,7 +558,10 @@ impl HtmlSafe for EmailField {} impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer + Display); /// Custom options for a [`IntegerField`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] +#[non_exhaustive] pub struct IntegerFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -664,7 +757,10 @@ impl_integer_as_form_field!(NonZeroUsize); impl_form_field!(BoolField, BoolFieldOptions, "a boolean"); /// Custom options for a [`BoolField`]. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] +#[non_exhaustive] pub struct BoolFieldOptions { /// If `true`, the field must be checked to be considered valid. pub must_be_true: Option, @@ -849,7 +945,10 @@ pub(crate) fn check_required(field: &T) -> Result<&str, FormFieldV impl_form_field!(FloatField, FloatFieldOptions, "a float", T: Float + Display); /// Custom options for a [`FloatField`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] +#[non_exhaustive] pub struct FloatFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -998,7 +1097,10 @@ impl_float_as_form_field!(f64); impl_form_field!(UrlField, UrlFieldOptions, "a URL"); /// Custom options for a [`UrlField`]. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] +#[non_exhaustive] pub struct UrlFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. @@ -1108,6 +1210,64 @@ impl AsFormField for Url { } } +impl_field_options_builder!( + StringFieldOptions, + StringFieldOptionsBuilder { + max_length, + min_length, + size, + autocapitalize, + autocomplete, + dir, + dirname, + list, + placeholder, + readonly + } +); +impl_field_options_builder!( + PasswordFieldOptions, + PasswordFieldOptionsBuilder { + max_length, + min_length, + size, + autocomplete, + placeholder, + readonly + } +); +impl_field_options_builder!( + EmailFieldOptions, + EmailFieldOptionsBuilder { + max_length, + min_length, + size, + autocomplete, + dir, + dirname, + list, + placeholder, + readonly + } +); +impl_field_options_builder!(IntegerFieldOptions, IntegerFieldOptionsBuilder{min, max, placeholder, readonly, step}); +impl_field_options_builder!(FloatFieldOptions, FloatFieldOptionsBuilder{min, max, placeholder, readonly, step}); +impl_field_options_builder!( + UrlFieldOptions, + UrlFieldOptionsBuilder { + max_length, + min_length, + size, + autocomplete, + dir, + dirname, + list, + placeholder, + readonly + } +); +impl_field_options_builder!(BoolFieldOptions, BoolFieldOptionsBuilder { must_be_true }); + #[cfg(test)] mod tests { use super::*; @@ -1121,18 +1281,18 @@ mod tests { name: "test".to_owned(), required: true, }, - StringFieldOptions { - max_length: Some(10), - min_length: Some(5), - size: Some(15), - autocapitalize: Some(AutoCapitalize::Words), - autocomplete: Some(AutoComplete::Value("foo bar".to_string())), - dir: Some(Dir::Ltr), - dirname: Some("dir".to_string()), - list: Some(List::new(["bar", "baz"])), - placeholder: Some("Enter text".to_string()), - readonly: Some(true), - }, + StringFieldOptions::builder() + .max_length(10) + .min_length(5) + .size(15) + .autocapitalize(AutoCapitalize::Words) + .autocomplete(AutoComplete::Value("foo bar".to_string())) + .dir(Dir::Ltr) + .dirname("dir".to_string()) + .list(List::new(["bar", "baz"])) + .placeholder("Enter text".to_string()) + .readonly(true) + .build(), ); let html = field.to_string(); assert!(html.contains("type=\"text\"")); @@ -1158,10 +1318,7 @@ mod tests { name: "test".to_owned(), required: true, }, - StringFieldOptions { - max_length: Some(10), - ..Default::default() - }, + StringFieldOptions::builder().max_length(10).build(), ); field .set_value(FormFieldValue::new_text("test")) @@ -1179,10 +1336,7 @@ mod tests { name: "test".to_owned(), required: true, }, - StringFieldOptions { - max_length: Some(10), - ..Default::default() - }, + StringFieldOptions::builder().max_length(10).build(), ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); let value = String::clean_value(&field); @@ -1197,14 +1351,14 @@ mod tests { name: "test".to_owned(), required: true, }, - PasswordFieldOptions { - max_length: Some(10), - min_length: Some(5), - size: Some(15), - autocomplete: Some(AutoComplete::Value("foo bar".to_string())), - placeholder: Some("Enter password".to_string()), - readonly: Some(false), - }, + PasswordFieldOptions::builder() + .max_length(10) + .min_length(5) + .size(15) + .autocomplete(AutoComplete::Value("foo bar".to_string())) + .placeholder("Enter password".to_string()) + .readonly(false) + .build(), ); let html = field.to_string(); assert!(html.contains("type=\"password\"")); @@ -1223,10 +1377,7 @@ mod tests { name: "test".to_owned(), required: true, }, - PasswordFieldOptions { - max_length: Some(10), - ..Default::default() - }, + PasswordFieldOptions::builder().max_length(10).build(), ); field .set_value(FormFieldValue::new_text("password")) @@ -1244,17 +1395,17 @@ mod tests { name: "test_name".to_owned(), required: true, }, - EmailFieldOptions { - max_length: Some(10), - min_length: Some(5), - size: Some(15), - autocomplete: Some(AutoComplete::Value("foo bar".to_string())), - dir: Some(Dir::Ltr), - dirname: Some("dir".to_string()), - list: Some(List::new(["foo@example.com", "baz@example.com"])), - placeholder: Some("Enter text".to_string()), - readonly: Some(true), - }, + EmailFieldOptions::builder() + .max_length(10) + .min_length(5) + .size(15) + .autocomplete(AutoComplete::Value("foo bar".to_string())) + .dir(Dir::Ltr) + .dirname("dir".to_string()) + .list(List::new(["foo@example.com", "baz@example.com"])) + .placeholder("Enter text".to_string()) + .readonly(true) + .build(), ); let html = field.to_string(); @@ -1282,11 +1433,10 @@ mod tests { name: "email_test".to_owned(), required: true, }, - EmailFieldOptions { - min_length: Some(10), - max_length: Some(50), - ..Default::default() - }, + EmailFieldOptions::builder() + .max_length(50) + .min_length(10) + .build(), ); field @@ -1306,11 +1456,10 @@ mod tests { name: "email_test".to_owned(), required: true, }, - EmailFieldOptions { - min_length: Some(10), - max_length: Some(50), - ..Default::default() - }, + EmailFieldOptions::builder() + .max_length(50) + .min_length(10) + .build(), ); field @@ -1330,11 +1479,10 @@ mod tests { name: "email_test".to_owned(), required: true, }, - EmailFieldOptions { - min_length: Some(5), - max_length: Some(10), - ..Default::default() - }, + EmailFieldOptions::builder() + .max_length(10) + .min_length(5) + .build(), ); field @@ -1357,11 +1505,10 @@ mod tests { name: "email_test".to_owned(), required: true, }, - EmailFieldOptions { - min_length: Some(5), - max_length: Some(10), - ..Default::default() - }, + EmailFieldOptions::builder() + .max_length(50) + .min_length(10) + .build(), ); field @@ -1384,11 +1531,10 @@ mod tests { name: "email_test".to_owned(), required: true, }, - EmailFieldOptions { - min_length: Some(50), - max_length: Some(10), - ..Default::default() - }, + EmailFieldOptions::builder() + .max_length(10) + .min_length(50) + .build(), ); field @@ -1412,13 +1558,13 @@ mod tests { name: "test".to_owned(), required: true, }, - IntegerFieldOptions { - min: Some(1), - max: Some(10), - placeholder: Some("Enter text".to_string()), - readonly: Some(false), - step: Some(Step::Value(10)), - }, + IntegerFieldOptions::builder() + .max(10) + .min(1) + .placeholder("Enter text".to_string()) + .readonly(false) + .step(Step::Value(10)) + .build(), ); let html = field.to_string(); @@ -1441,11 +1587,7 @@ mod tests { name: "test".to_owned(), required: true, }, - IntegerFieldOptions { - min: Some(1), - max: Some(10), - ..Default::default() - }, + IntegerFieldOptions::builder().max(10).min(1).build(), ); field .set_value(FormFieldValue::new_text("5")) @@ -1463,11 +1605,7 @@ mod tests { name: "test".to_owned(), required: true, }, - IntegerFieldOptions { - min: Some(10), - max: Some(50), - ..Default::default() - }, + IntegerFieldOptions::builder().min(10).max(50).build(), ); field .set_value(FormFieldValue::new_text("5")) @@ -1488,11 +1626,7 @@ mod tests { name: "test".to_owned(), required: true, }, - IntegerFieldOptions { - min: Some(10), - max: Some(50), - ..Default::default() - }, + IntegerFieldOptions::builder().min(10).max(50).build(), ); field .set_value(FormFieldValue::new_text("100")) @@ -1513,9 +1647,7 @@ mod tests { name: "test".to_owned(), required: true, }, - BoolFieldOptions { - must_be_true: Some(false), - }, + BoolFieldOptions::builder().must_be_true(false).build(), ); let html = field.to_string(); assert!(html.contains("type=\"checkbox\"")); @@ -1531,9 +1663,7 @@ mod tests { name: "test".to_owned(), required: true, }, - BoolFieldOptions { - must_be_true: Some(true), - }, + BoolFieldOptions::builder().must_be_true(true).build(), ); let html = field.to_string(); assert!(html.contains("type=\"checkbox\"")); @@ -1549,9 +1679,7 @@ mod tests { name: "test".to_owned(), required: true, }, - BoolFieldOptions { - must_be_true: Some(true), - }, + BoolFieldOptions::builder().must_be_true(false).build(), ); field .set_value(FormFieldValue::new_text("true")) @@ -1569,13 +1697,13 @@ mod tests { name: "test".to_owned(), required: true, }, - FloatFieldOptions { - min: Some(1.5), - max: Some(10.7), - placeholder: Some("Enter text".to_string()), - readonly: Some(true), - step: Some(Step::Any), - }, + FloatFieldOptions::builder() + .min(1.5) + .max(10.7) + .step(Step::Any) + .placeholder("Enter text".to_string()) + .readonly(true) + .build(), ); let html = field.to_string(); @@ -1599,11 +1727,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FloatFieldOptions { - min: Some(1.0), - max: Some(10.0), - ..Default::default() - }, + FloatFieldOptions::builder().min(1.0).max(10.0).build(), ); field .set_value(FormFieldValue::new_text("5.0")) @@ -1621,11 +1745,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FloatFieldOptions { - min: Some(5.0), - max: Some(10.0), - ..Default::default() - }, + FloatFieldOptions::builder().min(5.0).max(10.0).build(), ); field .set_value(FormFieldValue::new_text("2.0")) @@ -1646,11 +1766,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FloatFieldOptions { - min: Some(5.0), - max: Some(10.0), - ..Default::default() - }, + FloatFieldOptions::builder().min(5.0).max(10.0).build(), ); field .set_value(FormFieldValue::new_text("20.0")) @@ -1671,11 +1787,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FloatFieldOptions { - min: Some(1.0), - max: Some(10.0), - ..Default::default() - }, + FloatFieldOptions::builder().min(1.0).max(10.0).build(), ); let bad_inputs = ["NaN", "inf"]; @@ -1702,11 +1814,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FloatFieldOptions { - min: Some(1.0), - max: Some(10.0), - ..Default::default() - }, + FloatFieldOptions::builder().min(1.0).max(10.0).build(), ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); let value = f32::clean_value(&field); @@ -1721,17 +1829,17 @@ mod tests { name: "test".to_owned(), required: true, }, - UrlFieldOptions { - max_length: Some(100), - min_length: Some(5), - size: Some(30), - list: Some(List::new(["https://example.com"])), - dir: Some(Dir::Ltr), - dirname: Some("dir".to_owned()), - autocomplete: Some(AutoComplete::Value("url".to_owned())), - placeholder: Some("Enter URL".to_owned()), - readonly: Some(true), - }, + UrlFieldOptions::builder() + .max_length(100) + .min_length(5) + .size(30) + .autocomplete(AutoComplete::Value("url".to_string())) + .dir(Dir::Ltr) + .dirname("dir".to_string()) + .list(List::new(["http:://example.com"])) + .placeholder("Enter URL".to_string()) + .readonly(true) + .build(), ); field @@ -1754,17 +1862,17 @@ mod tests { name: "url".to_owned(), required: true, }, - UrlFieldOptions { - max_length: Some(120), - min_length: Some(10), - size: Some(40), - list: Some(List::new(["https://one.com", "https://two.com"])), - dir: Some(Dir::Ltr), - dirname: Some("lang".to_owned()), - autocomplete: Some(AutoComplete::Value("url".to_owned())), - placeholder: Some("Paste link".to_owned()), - readonly: Some(true), - }, + UrlFieldOptions::builder() + .max_length(120) + .min_length(10) + .size(40) + .list(List::new(["http://example.com", "https://example.org"])) + .dir(Dir::Ltr) + .dirname("lang".to_owned()) + .autocomplete(AutoComplete::Value("url".to_owned())) + .placeholder("Paste link".to_owned()) + .readonly(true) + .build(), ); field @@ -1798,17 +1906,16 @@ mod tests { name: "url".to_owned(), required: true, }, - UrlFieldOptions { - max_length: Some(50), - min_length: Some(5), - size: Some(20), - list: None, - dir: Some(Dir::Rtl), - dirname: Some("rtl".to_owned()), - autocomplete: Some(AutoComplete::Off), - placeholder: Some("Enter a link".to_owned()), - readonly: Some(false), - }, + UrlFieldOptions::builder() + .max_length(120) + .min_length(10) + .size(40) + .dir(Dir::Ltr) + .dirname("rtl".to_owned()) + .autocomplete(AutoComplete::Value("url".to_owned())) + .placeholder("Enter a link".to_owned()) + .readonly(false) + .build(), ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); diff --git a/cot/src/form/fields/chrono.rs b/cot/src/form/fields/chrono.rs index 3ffd9c8d7..efaf52fb7 100644 --- a/cot/src/form/fields/chrono.rs +++ b/cot/src/form/fields/chrono.rs @@ -9,8 +9,11 @@ use chrono_tz::Tz; use cot::form::FormField; use cot::form::fields::impl_form_field; use cot::html::HtmlTag; +use derive_builder::Builder; -use crate::form::fields::{SelectChoice, SelectField, Step, check_required}; +use crate::form::fields::{ + SelectChoice, SelectField, Step, check_required, impl_field_options_builder, +}; use crate::form::{AsFormField, FormFieldValidationError}; impl AsFormField for Weekday { @@ -126,12 +129,12 @@ impl_form_field!(DateTimeField, DateTimeFieldOptions, "a datetime"); /// let now = chrono::Local::now().naive_local(); /// let in_two_days = now + Duration::hours(48); /// -/// let options = DateTimeFieldOptions { -/// min: Some(now), -/// max: Some(in_two_days), -/// readonly: Some(true), -/// step: Some(Step::Value(Duration::seconds(300))), -/// }; +/// let options = DateTimeFieldOptions::builder() +/// .min(now) +/// .max(in_two_days) +/// .readonly(true) +/// .step(Step::Value(Duration::seconds(300))) +/// .build(); /// /// let field = DateTimeField::with_options( /// FormFieldOptions { @@ -142,7 +145,9 @@ impl_form_field!(DateTimeField, DateTimeFieldOptions, "a datetime"); /// options, /// ); /// ``` -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] #[non_exhaustive] pub struct DateTimeFieldOptions { /// The maximum datetime value of the field used to set the `max` attribute @@ -272,15 +277,11 @@ impl From for FormFieldValidationError { /// // we choose the later offset (i.e. `prefer_latest = true`). /// let tz: Tz = "America/New_York".parse().unwrap(); /// -/// let options = DateTimeWithTimezoneFieldOptions { -/// min: None, -/// max: None, -/// readonly: Some(false), -/// step: Some(Step::Value(Duration::seconds(60))), -/// timezone: Some(tz), +/// let options = DateTimeWithTimezoneFieldOptions::builder() +/// .timezone(tz) /// // If the given local time is ambiguous (DST fall‐back), pick the later of the two possibilities. -/// prefer_latest: Some(true), -/// }; +/// .prefer_latest(true) +/// .build(); /// /// let field = DateTimeWithTimezoneField::with_options( /// FormFieldOptions { @@ -291,7 +292,9 @@ impl From for FormFieldValidationError { /// options, /// ); /// ``` -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, Builder)] +#[builder(build_fn(skip, error= std::convert::Infallible))] +#[builder(setter(strip_option))] #[non_exhaustive] pub struct DateTimeWithTimezoneFieldOptions { /// The maximum allowed datetime (with offset) for this field. @@ -461,12 +464,12 @@ impl_form_field!(TimeField, TimeFieldOptions, "a time"); /// use cot::form::fields::{Step, TimeField, TimeFieldOptions}; /// use cot::form::{FormField, FormFieldOptions}; /// -/// let options = TimeFieldOptions { -/// min: Some(NaiveTime::from_hms_opt(9, 0, 0).unwrap()), -/// max: Some(NaiveTime::from_hms_opt(17, 0, 0).unwrap()), -/// readonly: Some(true), -/// step: Some(Step::Value(Duration::seconds(900))), -/// }; +/// let options = TimeFieldOptions::builder() +/// .min(NaiveTime::from_hms_opt(9, 0, 0).unwrap()) +/// .max(NaiveTime::from_hms_opt(17, 0, 0).unwrap()) +/// .readonly(true) +/// .step(Step::Value(Duration::seconds(900))) +/// .build(); /// /// let field = TimeField::with_options( /// FormFieldOptions { @@ -477,7 +480,9 @@ impl_form_field!(TimeField, TimeFieldOptions, "a time"); /// options, /// ); /// ``` -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, Builder)] +#[builder(build_fn(skip, error= std::convert::Infallible))] +#[builder(setter(strip_option))] #[non_exhaustive] pub struct TimeFieldOptions { /// The maximum time value of the field used to set the `max` attribute @@ -583,12 +588,12 @@ impl_form_field!(DateField, DateFieldOptions, "a date"); /// use cot::form::fields::{DateField, DateFieldOptions, Step}; /// use cot::form::{FormField, FormFieldOptions}; /// -/// let options = DateFieldOptions { -/// min: Some(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()), -/// max: Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()), -/// readonly: None, -/// step: Some(Step::Value(Duration::days(7))), -/// }; +/// let options = DateFieldOptions::builder() +/// .min(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()) +/// .max(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()) +/// .readonly(true) +/// .step(Step::Value(Duration::days(7))) +/// .build(); /// /// let field = DateField::with_options( /// FormFieldOptions { @@ -599,7 +604,9 @@ impl_form_field!(DateField, DateFieldOptions, "a date"); /// options, /// ); /// ``` -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, Builder)] +#[builder(build_fn(skip, error= std::convert::Infallible))] +#[builder(setter(strip_option))] #[non_exhaustive] pub struct DateFieldOptions { /// The maximum date value of the field used to set the `max` attribute @@ -692,6 +699,45 @@ impl AsFormField for NaiveDate { impl HtmlSafe for DateField {} +impl_field_options_builder!( + DateTimeFieldOptions, + DateTimeFieldOptionsBuilder { + max, + min, + readonly, + step + } +); +impl_field_options_builder!( + DateTimeWithTimezoneFieldOptions, + DateTimeWithTimezoneFieldOptionsBuilder { + max, + min, + readonly, + step, + timezone, + prefer_latest + } +); +impl_field_options_builder!( + TimeFieldOptions, + TimeFieldOptionsBuilder { + max, + min, + readonly, + step + } +); +impl_field_options_builder!( + DateFieldOptions, + DateFieldOptionsBuilder { + max, + min, + readonly, + step + } +); + #[cfg(test)] mod tests { use std::collections::{HashSet, LinkedList, VecDeque}; @@ -1126,18 +1172,18 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeFieldOptions { - min: Some( - NaiveDateTime::parse_from_str("2025-05-27T00:00:00", "%Y-%m-%dT%H:%M:%S") - .unwrap(), - ), - max: Some( + DateTimeFieldOptions::builder() + .max( NaiveDateTime::parse_from_str("2025-05-28T00:00:00", "%Y-%m-%dT%H:%M:%S") .unwrap(), - ), - readonly: Some(true), - step: Some(Step::Value(Duration::seconds(60))), - }, + ) + .min( + NaiveDateTime::parse_from_str("2025-05-27T00:00:00", "%Y-%m-%dT%H:%M:%S") + .unwrap(), + ) + .readonly(true) + .step(Step::Value(Duration::seconds(60))) + .build(), ); let html = field.to_string(); assert!(html.contains("type=\"datetime-local\"")); @@ -1157,18 +1203,17 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeFieldOptions { - min: Some( + DateTimeFieldOptions::builder() + .min( NaiveDateTime::parse_from_str("2025-05-27T00:00:00", "%Y-%m-%dT%H:%M:%S") .unwrap(), - ), - max: Some( + ) + .max( NaiveDateTime::parse_from_str("2025-05-28T00:00:00", "%Y-%m-%dT%H:%M:%S") .unwrap(), - ), - readonly: None, - step: Some(Step::Value(Duration::seconds(60))), - }, + ) + .step(Step::Value(Duration::seconds(60))) + .build(), ); for &dt in &["2025-05-27T12:34", "2025-05-27T12:34:00"] { @@ -1186,15 +1231,13 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeFieldOptions { - min: Some( + DateTimeFieldOptions::builder() + .min( NaiveDateTime::parse_from_str("2025-05-27T10:00:00", "%Y-%m-%dT%H:%M:%S") .unwrap(), - ), - max: None, - readonly: None, - step: Some(Step::Value(Duration::seconds(60))), - }, + ) + .step(Step::Value(Duration::seconds(60))) + .build(), ); for &dt in &["2025-05-27T09:59", "2025-05-27T09:59:00"] { field.set_value(FormFieldValue::new_text(dt)).await.unwrap(); @@ -1214,15 +1257,13 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeFieldOptions { - min: None, - max: Some( + DateTimeFieldOptions::builder() + .max( NaiveDateTime::parse_from_str("2025-05-27T10:00:00", "%Y-%m-%dT%H:%M:%S") .unwrap(), - ), - readonly: None, - step: Some(Step::Value(Duration::seconds(60))), - }, + ) + .step(Step::Value(Duration::seconds(60))) + .build(), ); for &dt in &["2025-05-27T10:01", "2025-05-27T10:01:00"] { field.set_value(FormFieldValue::new_text(dt)).await.unwrap(); @@ -1242,20 +1283,24 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: Some( - DateTime::parse_from_str("2025-05-27T00:00:00 +0000", "%Y-%m-%dT%H:%M:%S %z") - .unwrap(), - ), - max: Some( - DateTime::parse_from_str("2025-05-28T00:00:00 +0000", "%Y-%m-%dT%H:%M:%S %z") - .unwrap(), - ), - readonly: Some(true), - step: Some(Step::Value(Duration::seconds(60))), - timezone: None, - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder() + .min( + DateTime::::parse_from_str( + "2025-05-27T00:00:00 +0000", + "%Y-%m-%dT%H:%M:%S %z", + ) + .unwrap(), + ) + .max( + DateTime::::parse_from_str( + "2025-05-28T00:00:00 +0000", + "%Y-%m-%dT%H:%M:%S %z", + ) + .unwrap(), + ) + .readonly(true) + .step(Step::Value(Duration::seconds(60))) + .build(), ); let html = field.to_string(); assert_eq!( @@ -1272,14 +1317,7 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: None, - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder().build(), ); field .set_value(FormFieldValue::new_text("2025-05-27T12:34")) @@ -1299,14 +1337,9 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: Some(offset), - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder() + .timezone(offset) + .build(), ); field .set_value(FormFieldValue::new_text("2025-05-27T01:23")) @@ -1326,14 +1359,10 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: Some(offset), - prefer_latest: Some(false), - }, + DateTimeWithTimezoneFieldOptions::builder() + .timezone(offset) + .prefer_latest(false) + .build(), ); field .set_value(FormFieldValue::new_text("2024-11-03T01:30")) @@ -1353,14 +1382,10 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: Some(offset), - prefer_latest: Some(true), - }, + DateTimeWithTimezoneFieldOptions::builder() + .timezone(offset) + .prefer_latest(true) + .build(), ); field .set_value(FormFieldValue::new_text("2024-11-03T01:30")) @@ -1380,14 +1405,9 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: Some(offset), - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder() + .timezone(offset) + .build(), ); field .set_value(FormFieldValue::new_text("2024-11-03T01:30")) @@ -1409,14 +1429,9 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: Some(offset), - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder() + .timezone(offset) + .build(), ); field .set_value(FormFieldValue::new_text("2024-03-10T02:30")) @@ -1440,14 +1455,9 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: Some(min_dt), - max: None, - readonly: None, - step: None, - timezone: None, - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder() + .min(min_dt) + .build(), ); field .set_value(FormFieldValue::new_text("2025-05-27T09:59")) @@ -1470,14 +1480,9 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: Some(max_dt), - readonly: None, - step: None, - timezone: None, - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder() + .max(max_dt) + .build(), ); field .set_value(FormFieldValue::new_text("2025-05-27T10:01")) @@ -1498,14 +1503,7 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: None, - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder().build(), ); field .set_value(FormFieldValue::new_text("not-a-valid-datetime")) @@ -1524,14 +1522,7 @@ mod tests { name: "dt".into(), required: true, }, - DateTimeWithTimezoneFieldOptions { - min: None, - max: None, - readonly: None, - step: None, - timezone: None, - prefer_latest: None, - }, + DateTimeWithTimezoneFieldOptions::builder().build(), ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -1547,12 +1538,12 @@ mod tests { name: "time".into(), required: true, }, - TimeFieldOptions { - min: Some(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()), - max: Some(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()), - readonly: Some(false), - step: Some(Step::Value(Duration::seconds(60))), - }, + TimeFieldOptions::builder() + .min(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()) + .max(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()) + .step(Step::Value(Duration::seconds(60))) + .readonly(false) + .build(), ); let html = field.to_string(); assert!(html.contains("type=\"time\"")); @@ -1571,12 +1562,12 @@ mod tests { name: "t".into(), required: true, }, - TimeFieldOptions { - min: Some(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()), - max: Some(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()), - readonly: Some(false), - step: Some(Step::Value(Duration::seconds(60))), - }, + TimeFieldOptions::builder() + .min(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()) + .max(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()) + .step(Step::Value(Duration::seconds(60))) + .readonly(false) + .build(), ); field .set_value(FormFieldValue::new_text("12:30")) @@ -1594,12 +1585,11 @@ mod tests { name: "t".into(), required: true, }, - TimeFieldOptions { - min: Some(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()), - max: None, - readonly: Some(false), - step: Some(Step::Value(Duration::seconds(60))), - }, + TimeFieldOptions::builder() + .min(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()) + .step(Step::Value(Duration::seconds(60))) + .readonly(false) + .build(), ); for &time in &["08:59:00", "08:59"] { field @@ -1622,12 +1612,11 @@ mod tests { name: "t".into(), required: true, }, - TimeFieldOptions { - min: None, - max: Some(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()), - readonly: Some(false), - step: Some(Step::Value(Duration::seconds(60))), - }, + TimeFieldOptions::builder() + .max(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()) + .step(Step::Value(Duration::seconds(60))) + .readonly(false) + .build(), ); for &time in &["17:01:00", "17:01"] { @@ -1651,12 +1640,11 @@ mod tests { name: "d".into(), required: true, }, - DateFieldOptions { - min: Some(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()), - max: Some(NaiveDate::parse_from_str("2025-05-28", "%Y-%m-%d").unwrap()), - readonly: None, - step: Some(Step::Value(Duration::days(1))), - }, + DateFieldOptions::builder() + .min(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) + .max(NaiveDate::parse_from_str("2025-05-28", "%Y-%m-%d").unwrap()) + .step(Step::Value(Duration::days(1))) + .build(), ); let html = field.to_string(); assert!(html.contains("type=\"date\"")); @@ -1673,12 +1661,11 @@ mod tests { name: "d".into(), required: true, }, - DateFieldOptions { - min: Some(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()), - max: Some(NaiveDate::parse_from_str("2025-05-28", "%Y-%m-%d").unwrap()), - readonly: None, - step: Some(Step::Value(Duration::days(1))), - }, + DateFieldOptions::builder() + .min(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) + .max(NaiveDate::parse_from_str("2025-05-28", "%Y-%m-%d").unwrap()) + .step(Step::Value(Duration::days(1))) + .build(), ); field .set_value(FormFieldValue::new_text("2025-05-27")) @@ -1696,12 +1683,10 @@ mod tests { name: "d".into(), required: true, }, - DateFieldOptions { - min: Some(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()), - max: None, - readonly: None, - step: Some(Step::Value(Duration::days(1))), - }, + DateFieldOptions::builder() + .min(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) + .step(Step::Value(Duration::days(1))) + .build(), ); field .set_value(FormFieldValue::new_text("2025-05-26")) @@ -1722,12 +1707,10 @@ mod tests { name: "d".into(), required: true, }, - DateFieldOptions { - min: None, - max: Some(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()), - readonly: None, - step: Some(Step::Value(Duration::days(1))), - }, + DateFieldOptions::builder() + .max(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) + .step(Step::Value(Duration::days(1))) + .build(), ); field .set_value(FormFieldValue::new_text("2025-05-28")) diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index ebf9c57a5..3dedb9779 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -2,8 +2,10 @@ use std::fmt::{Display, Formatter}; use askama::filters::HtmlSafe; use bytes::Bytes; +use cot::form::fields::impl_field_options_builder; use cot::form::{AsFormField, FormFieldValidationError}; use cot::html::HtmlTag; +use derive_builder::Builder; use crate::form::fields::attrs::Capture; use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; @@ -52,7 +54,9 @@ impl FormField for FileField { } /// Custom options for a [`FileField`]. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] #[non_exhaustive] pub struct FileFieldOptions { /// The accepted file types. Used to set the [`accept` attribute] in the @@ -150,6 +154,11 @@ impl InMemoryUploadedFile { } } +impl_field_options_builder!( + FileFieldOptions, + FileFieldOptionsBuilder { accept, capture } +); + #[cfg(test)] mod tests { use bytes::Bytes; @@ -167,10 +176,10 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { - accept: Some(vec!["image/*".to_string(), ".pdf".to_string()]), - capture: Some(Capture::Environment), - }, + FileFieldOptions::builder() + .accept(vec!["image/*".to_string(), ".pdf".to_string()]) + .capture(Capture::Environment) + .build(), ); let html = field.to_string(); @@ -189,10 +198,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { - accept: None, - capture: Some(Capture::User), - }, + FileFieldOptions::builder().capture(Capture::User).build(), ); let html = field.to_string(); @@ -211,9 +217,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { - ..Default::default() - }, + FileFieldOptions::builder().build(), ); let boundary = "boundary"; @@ -248,9 +252,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { - ..Default::default() - }, + FileFieldOptions::builder().build(), ); let value = InMemoryUploadedFile::clean_value(&field); diff --git a/cot/src/form/fields/select.rs b/cot/src/form/fields/select.rs index 75140743d..9528b855f 100644 --- a/cot/src/form/fields/select.rs +++ b/cot/src/form/fields/select.rs @@ -1,6 +1,7 @@ use std::fmt::{Debug, Display, Formatter}; use askama::filters::HtmlSafe; +use cot::form::fields::impl_field_options_builder; /// Derive helper that implements `AsFormField` for select-like enums and common /// collections. /// @@ -131,6 +132,7 @@ pub use cot_macros::SelectAsFormField; /// [`SelectField`]: cot::form::fields::SelectField /// [`SelectMultipleField`]: cot::form::fields::SelectMultipleField pub use cot_macros::SelectChoice; +use derive_builder::Builder; use indexmap::IndexSet; use crate::form::fields::impl_form_field; @@ -183,7 +185,9 @@ pub(crate) use impl_as_form_field_mult_collection; impl_form_field!(SelectField, SelectFieldOptions, "a dropdown list", T: SelectChoice + Send); /// Custom options for a [`SelectField`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] #[non_exhaustive] pub struct SelectFieldOptions { /// The list of available choices for the select field. @@ -282,7 +286,9 @@ impl FormField for SelectMultipleField { } /// Custom options for a [`SelectMultipleField`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[builder(setter(strip_option))] #[non_exhaustive] pub struct SelectMultipleFieldOptions { /// The list of available choices for the multi-select field. @@ -623,6 +629,8 @@ pub trait SelectChoice { fn to_string(&self) -> String; } +impl_field_options_builder!(SelectFieldOptions, SelectFieldOptionsBuilder{choices, none_option}); +impl_field_options_builder!(SelectMultipleFieldOptions, SelectMultipleFieldOptionsBuilder{choices, size}); #[cfg(test)] mod tests { use std::collections::{HashSet, LinkedList, VecDeque}; @@ -719,10 +727,9 @@ mod tests { name: "test_select".to_owned(), required: false, }, - SelectFieldOptions { - choices: None, - none_option: Some("Please select...".to_string()), - }, + SelectFieldOptions::builder() + .none_option("Please select...".to_owned()) + .build(), ); let html = field.to_string(); @@ -738,10 +745,9 @@ mod tests { name: "test_select".to_owned(), required: false, }, - SelectFieldOptions { - choices: Some(vec![TestChoice::Option1, TestChoice::Option3]), - none_option: None, - }, + SelectFieldOptions::builder() + .choices(vec![TestChoice::Option1, TestChoice::Option3]) + .build(), ); let html = field.to_string(); @@ -800,10 +806,7 @@ mod tests { name: "test_multi".to_owned(), required: false, }, - SelectMultipleFieldOptions { - choices: None, - size: Some(5), - }, + SelectMultipleFieldOptions::builder().size(5).build(), ); let html = field.to_string(); From ec769e024298b0262b86a635c0e118f34bcde95f Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 8 Jul 2026 01:40:00 +0000 Subject: [PATCH 15/19] path cleanup --- cot/src/form/fields.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 3de29a691..fea8882f1 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -10,17 +10,18 @@ use std::num::{ }; use askama::filters::HtmlSafe; -pub use attrs::{AutoCapitalize, AutoComplete, Dir, List, Step}; +pub use attrs::{AutoCapitalize, AutoComplete, Capture, Dir, List, Step}; pub use chrono::{ - DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, - DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, + DateField, DateFieldOptions, DateFieldOptionsBuilder, DateTimeField, DateTimeFieldOptions, + DateTimeFieldOptionsBuilder, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, + DateTimeWithTimezoneFieldOptionsBuilder, TimeField, TimeFieldOptions, TimeFieldOptionsBuilder, }; use derive_builder::Builder; -pub use files::{FileField, FileFieldOptions, InMemoryUploadedFile}; +pub use files::{FileField, FileFieldOptions, FileFieldOptionsBuilder, InMemoryUploadedFile}; pub(crate) use select::check_required_multiple; pub use select::{ - SelectAsFormField, SelectChoice, SelectField, SelectFieldOptions, SelectMultipleField, - SelectMultipleFieldOptions, + SelectAsFormField, SelectChoice, SelectField, SelectFieldOptions, SelectFieldOptionsBuilder, + SelectMultipleField, SelectMultipleFieldOptions, SelectMultipleFieldOptionsBuilder, }; use crate::auth::PasswordHash; From d2b10bc4dce84b83a3361866c7102f1ba073e945 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 8 Jul 2026 01:59:30 +0000 Subject: [PATCH 16/19] clippy fix --- cot/src/form/fields/attrs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index 4a137de0c..cbc705638 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -200,6 +200,7 @@ pub enum Capture { impl Capture { /// Returns the string representation for use in HTML. + #[must_use] pub fn as_str(self) -> &'static str { match self { Self::User => "user", From 357641b82b2891927b93fa93ef2a8408700af868 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 10 Jul 2026 10:45:06 +0000 Subject: [PATCH 17/19] add builder to FormFieldOptions --- cot-macros/src/form.rs | 11 +- cot/src/form.rs | 50 ++++- cot/src/form/fields.rs | 270 +++++++++++------------ cot/src/form/fields/chrono.rs | 400 +++++++++++++++++----------------- cot/src/form/fields/files.rs | 40 ++-- cot/src/form/fields/select.rs | 230 +++++++++---------- 6 files changed, 525 insertions(+), 476 deletions(-) diff --git a/cot-macros/src/form.rs b/cot-macros/src/form.rs index 37cdfcaf6..7c2a77fb4 100644 --- a/cot-macros/src/form.rs +++ b/cot-macros/src/form.rs @@ -135,11 +135,12 @@ impl FormDeriveBuilder { Vec::new() }; quote!(#field_ident: { - let options = #crate_ident::form::FormFieldOptions { - id: stringify!(#field_ident).to_owned(), - name: #name.to_owned(), - required: true, - }; + let options = #crate_ident::form::FormFieldOptions::builder() + .id(stringify!(#field_ident).to_owned()) + .name(#name.to_owned()) + .required(true) + .build(); + type Field = <#ty as #crate_ident::form::AsFormField>::Type; type CustomOptions = ::CustomOptions; let mut custom_options: CustomOptions = ::core::default::Default::default(); diff --git a/cot/src/form.rs b/cot/src/form.rs index fcfb62fad..499045e88 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -58,6 +58,7 @@ use cot_core::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYP /// fields. If the form fields are not safe to render as HTML, the form context /// will not be safe to render as HTML either. pub use cot_macros::Form; +use derive_builder::Builder; use derive_more::with_trait::Debug; pub use field_value::{FormFieldValue, FormFieldValueError}; use http_body_util::BodyExt; @@ -508,7 +509,9 @@ pub trait FormContext: Debug { } /// Generic options valid for all types of form fields. -#[derive(Debug)] +#[derive(Debug, Default, Clone, Builder)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[non_exhaustive] pub struct FormFieldOptions { /// The HTML ID of the form field. pub id: String, @@ -521,6 +524,51 @@ pub struct FormFieldOptions { pub required: bool, } +impl FormFieldOptions { + /// Creates a new [`FormFieldOptionsBuilder`] to build a + /// [`FormFieldOptions`]. + /// + /// # Examples + /// + /// ``` + /// use cot::form::FormFieldOptions; + /// + /// let options = FormFieldOptions::builder() + /// .name("field_name".to_owned()) + /// .id("field_id".to_owned()) + /// .required(true) + /// .build(); + /// ``` + #[must_use] + pub fn builder() -> FormFieldOptionsBuilder { + FormFieldOptionsBuilder::default() + } +} +impl FormFieldOptionsBuilder { + /// Builds the [`FormFieldOptions`], falling back to defaults for any field + /// that wasn't explicitly set. + /// + /// # Examples + /// + /// ``` + /// use cot::form::FormFieldOptions; + /// + /// let options = FormFieldOptions::builder() + /// .name("field_name".to_owned()) + /// .id("field_id".to_owned()) + /// .required(true) + /// .build(); + /// ``` + #[must_use] + pub fn build(&self) -> FormFieldOptions { + FormFieldOptions { + id: self.id.clone().unwrap_or_default(), + name: self.name.clone().unwrap_or_default(), + required: self.required.unwrap_or_default(), + } + } +} + /// A form field. /// /// This trait is used to define a type of field that can be used in a form. It diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index fea8882f1..b0623a541 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -1277,11 +1277,11 @@ mod tests { #[test] fn string_field_render() { let field = StringField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), StringFieldOptions::builder() .max_length(10) .min_length(5) @@ -1314,11 +1314,11 @@ mod tests { #[cot::test] async fn string_field_clean_value() { let mut field = StringField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), StringFieldOptions::builder().max_length(10).build(), ); field @@ -1332,11 +1332,11 @@ mod tests { #[cot::test] async fn string_field_clean_required() { let mut field = StringField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), StringFieldOptions::builder().max_length(10).build(), ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -1347,11 +1347,11 @@ mod tests { #[test] fn password_field_render() { let field = PasswordField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), PasswordFieldOptions::builder() .max_length(10) .min_length(5) @@ -1373,11 +1373,11 @@ mod tests { #[cot::test] async fn password_field_clean_value() { let mut field = PasswordField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), PasswordFieldOptions::builder().max_length(10).build(), ); field @@ -1391,11 +1391,11 @@ mod tests { #[test] fn email_field_render() { let field = EmailField::with_options( - FormFieldOptions { - id: "test_id".to_owned(), - name: "test_name".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test_id".to_owned()) + .name("test_name".to_owned()) + .required(true) + .build(), EmailFieldOptions::builder() .max_length(10) .min_length(5) @@ -1429,11 +1429,11 @@ mod tests { #[cot::test] async fn email_field_clean_valid() { let mut field = EmailField::with_options( - FormFieldOptions { - id: "email_test".to_owned(), - name: "email_test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("email_test".to_owned()) + .name("email_test".to_owned()) + .required(true) + .build(), EmailFieldOptions::builder() .max_length(50) .min_length(10) @@ -1452,11 +1452,11 @@ mod tests { #[cot::test] async fn email_field_clean_invalid_format() { let mut field = EmailField::with_options( - FormFieldOptions { - id: "email_test".to_owned(), - name: "email_test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("email_test".to_owned()) + .name("email_test".to_owned()) + .required(true) + .build(), EmailFieldOptions::builder() .max_length(50) .min_length(10) @@ -1475,11 +1475,11 @@ mod tests { #[cot::test] async fn email_field_clean_exceeds_max_length() { let mut field = EmailField::with_options( - FormFieldOptions { - id: "email_test".to_owned(), - name: "email_test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("email_test".to_owned()) + .name("email_test".to_owned()) + .required(true) + .build(), EmailFieldOptions::builder() .max_length(10) .min_length(5) @@ -1501,11 +1501,11 @@ mod tests { #[cot::test] async fn email_field_clean_below_min_length() { let mut field = EmailField::with_options( - FormFieldOptions { - id: "email_test".to_owned(), - name: "email_test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("email_test".to_owned()) + .name("email_test".to_owned()) + .required(true) + .build(), EmailFieldOptions::builder() .max_length(50) .min_length(10) @@ -1527,11 +1527,11 @@ mod tests { #[cot::test] async fn email_field_clean_invalid_length_options() { let mut field = EmailField::with_options( - FormFieldOptions { - id: "email_test".to_owned(), - name: "email_test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("email_test".to_owned()) + .name("email_test".to_owned()) + .required(true) + .build(), EmailFieldOptions::builder() .max_length(10) .min_length(50) @@ -1554,11 +1554,11 @@ mod tests { #[test] fn integer_field_render() { let field = IntegerField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), IntegerFieldOptions::builder() .max(10) .min(1) @@ -1583,11 +1583,11 @@ mod tests { #[cot::test] async fn integer_field_clean_value() { let mut field = IntegerField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), IntegerFieldOptions::builder().max(10).min(1).build(), ); field @@ -1601,11 +1601,11 @@ mod tests { #[cot::test] async fn integer_field_clean_value_below_min_value() { let mut field = IntegerField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), IntegerFieldOptions::builder().min(10).max(50).build(), ); field @@ -1622,11 +1622,11 @@ mod tests { #[cot::test] async fn integer_field_clean_value_above_max_value() { let mut field = IntegerField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), IntegerFieldOptions::builder().min(10).max(50).build(), ); field @@ -1643,11 +1643,11 @@ mod tests { #[test] fn bool_field_render() { let field = BoolField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), BoolFieldOptions::builder().must_be_true(false).build(), ); let html = field.to_string(); @@ -1659,11 +1659,11 @@ mod tests { #[test] fn bool_field_render_must_be_true() { let field = BoolField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), BoolFieldOptions::builder().must_be_true(true).build(), ); let html = field.to_string(); @@ -1675,11 +1675,11 @@ mod tests { #[cot::test] async fn bool_field_clean_value() { let mut field = BoolField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), BoolFieldOptions::builder().must_be_true(false).build(), ); field @@ -1693,11 +1693,11 @@ mod tests { #[test] fn float_field_render() { let field = FloatField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), FloatFieldOptions::builder() .min(1.5) .max(10.7) @@ -1723,11 +1723,11 @@ mod tests { #[expect(clippy::float_cmp)] async fn float_field_clean_value() { let mut field = FloatField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), FloatFieldOptions::builder().min(1.0).max(10.0).build(), ); field @@ -1741,11 +1741,11 @@ mod tests { #[cot::test] async fn float_field_clean_value_min_value_not_met() { let mut field = FloatField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), FloatFieldOptions::builder().min(5.0).max(10.0).build(), ); field @@ -1762,11 +1762,11 @@ mod tests { #[cot::test] async fn float_field_clean_value_max_value_exceeded() { let mut field = FloatField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), FloatFieldOptions::builder().min(5.0).max(10.0).build(), ); field @@ -1783,11 +1783,11 @@ mod tests { #[cot::test] async fn float_field_clean_value_nan_and_inf() { let mut field = FloatField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), FloatFieldOptions::builder().min(1.0).max(10.0).build(), ); let bad_inputs = ["NaN", "inf"]; @@ -1810,11 +1810,11 @@ mod tests { #[cot::test] async fn float_field_clean_required() { let mut field = FloatField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), FloatFieldOptions::builder().min(1.0).max(10.0).build(), ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -1825,11 +1825,11 @@ mod tests { #[cot::test] async fn url_field_clean_value() { let mut field = UrlField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), UrlFieldOptions::builder() .max_length(100) .min_length(5) @@ -1858,11 +1858,11 @@ mod tests { #[cot::test] async fn url_field_render() { let mut field = UrlField::with_options( - FormFieldOptions { - id: "id_url".to_owned(), - name: "url".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("id_url".to_owned()) + .name("url".to_owned()) + .required(true) + .build(), UrlFieldOptions::builder() .max_length(120) .min_length(10) @@ -1902,11 +1902,11 @@ mod tests { #[cot::test] async fn url_field_clean_required() { let mut field = UrlField::with_options( - FormFieldOptions { - id: "id_url".to_owned(), - name: "url".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("id_url".to_owned()) + .name("url".to_owned()) + .required(true) + .build(), UrlFieldOptions::builder() .max_length(120) .min_length(10) diff --git a/cot/src/form/fields/chrono.rs b/cot/src/form/fields/chrono.rs index efaf52fb7..d1dea40c4 100644 --- a/cot/src/form/fields/chrono.rs +++ b/cot/src/form/fields/chrono.rs @@ -137,11 +137,11 @@ impl_form_field!(DateTimeField, DateTimeFieldOptions, "a datetime"); /// .build(); /// /// let field = DateTimeField::with_options( -/// FormFieldOptions { -/// id: "event_time".into(), -/// name: "event_time".into(), -/// required: true, -/// }, +/// FormFieldOptions::builder() +/// .id("event_time".into()) +/// .name("event_time".into()) +/// .required(true) +/// .build(), /// options, /// ); /// ``` @@ -284,11 +284,11 @@ impl From for FormFieldValidationError { /// .build(); /// /// let field = DateTimeWithTimezoneField::with_options( -/// FormFieldOptions { -/// id: "dt".into(), -/// name: "dt".into(), -/// required: true, -/// }, +/// FormFieldOptions::builder() +/// .id("dt".into()) +/// .name("dt".into()) +/// .required(true) +/// .build(), /// options, /// ); /// ``` @@ -472,11 +472,11 @@ impl_form_field!(TimeField, TimeFieldOptions, "a time"); /// .build(); /// /// let field = TimeField::with_options( -/// FormFieldOptions { -/// id: "event_time".into(), -/// name: "event_time".into(), -/// required: true, -/// }, +/// FormFieldOptions::builder() +/// .id("event_time".into()) +/// .name("event_time".into()) +/// .required(true) +/// .build(), /// options, /// ); /// ``` @@ -596,11 +596,11 @@ impl_form_field!(DateField, DateFieldOptions, "a date"); /// .build(); /// /// let field = DateField::with_options( -/// FormFieldOptions { -/// id: "event_time".into(), -/// name: "event_time".into(), -/// required: true, -/// }, +/// FormFieldOptions::builder() +/// .id("event_time".into()) +/// .name("event_time".into()) +/// .required(true) +/// .build(), /// options, /// ); /// ``` @@ -822,11 +822,11 @@ mod tests { #[cot::test] async fn weekday_as_form_field_clean_value() { let mut field = SelectField::::with_options( - FormFieldOptions { - id: "weekday".to_owned(), - name: "weekday".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("weekday".to_owned()) + .name("weekday".to_owned()) + .required(true) + .build(), SelectFieldOptions::default(), ); @@ -842,11 +842,11 @@ mod tests { #[cot::test] async fn weekday_as_form_field_clean_value_invalid() { let mut field = SelectField::::with_options( - FormFieldOptions { - id: "weekday".to_owned(), - name: "weekday".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("weekday".to_owned()) + .name("weekday".to_owned()) + .required(true) + .build(), SelectFieldOptions::default(), ); @@ -867,11 +867,11 @@ mod tests { #[cot::test] async fn weekday_as_form_field_clean_value_required_empty() { let mut field = SelectField::::with_options( - FormFieldOptions { - id: "weekday".to_owned(), - name: "weekday".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("weekday".to_owned()) + .name("weekday".to_owned()) + .required(true) + .build(), SelectFieldOptions::default(), ); @@ -891,11 +891,11 @@ mod tests { #[cot::test] async fn weekday_vec_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -922,11 +922,11 @@ mod tests { #[cot::test] async fn weekday_vec_as_form_field_clean_value_empty_required() { let field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -937,11 +937,11 @@ mod tests { #[cot::test] async fn weekday_vec_as_form_field_clean_value_invalid() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -972,11 +972,11 @@ mod tests { #[cot::test] async fn weekday_hash_set_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -998,11 +998,11 @@ mod tests { #[cot::test] async fn weekday_vec_deque_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1024,11 +1024,11 @@ mod tests { #[cot::test] async fn weekday_linked_list_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1045,11 +1045,11 @@ mod tests { #[cot::test] async fn weekday_index_set_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1075,11 +1075,11 @@ mod tests { #[cot::test] async fn weekday_set_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1115,11 +1115,11 @@ mod tests { #[test] fn weekday_select_field_render() { let field = SelectField::::with_options( - FormFieldOptions { - id: "weekday".to_owned(), - name: "weekday".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekday".to_owned()) + .name("weekday".to_owned()) + .required(false) + .build(), SelectFieldOptions::default(), ); @@ -1146,11 +1146,11 @@ mod tests { #[test] fn weekday_select_multiple_field_render() { let field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "weekdays".to_owned(), - name: "weekdays".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("weekdays".to_owned()) + .name("weekdays".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1167,11 +1167,11 @@ mod tests { #[test] fn datetime_field_render() { let field = DateTimeField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeFieldOptions::builder() .max( NaiveDateTime::parse_from_str("2025-05-28T00:00:00", "%Y-%m-%dT%H:%M:%S") @@ -1198,11 +1198,11 @@ mod tests { #[cot::test] async fn datetime_field_clean_valid() { let mut field = DateTimeField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeFieldOptions::builder() .min( NaiveDateTime::parse_from_str("2025-05-27T00:00:00", "%Y-%m-%dT%H:%M:%S") @@ -1226,11 +1226,11 @@ mod tests { #[cot::test] async fn datetime_field_clean_below_min() { let mut field = DateTimeField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeFieldOptions::builder() .min( NaiveDateTime::parse_from_str("2025-05-27T10:00:00", "%Y-%m-%dT%H:%M:%S") @@ -1252,11 +1252,11 @@ mod tests { #[cot::test] async fn datetime_field_clean_above_max() { let mut field = DateTimeField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeFieldOptions::builder() .max( NaiveDateTime::parse_from_str("2025-05-27T10:00:00", "%Y-%m-%dT%H:%M:%S") @@ -1278,11 +1278,11 @@ mod tests { #[test] fn datetime_with_tz_field_render() { let field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .min( DateTime::::parse_from_str( @@ -1312,11 +1312,11 @@ mod tests { #[cot::test] async fn datetime_with_tz_clean_valid_default_utc() { let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder().build(), ); field @@ -1332,11 +1332,11 @@ mod tests { async fn datetime_with_tz_clean_valid_custom_offset() { let offset = Tz::America__New_York; let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .timezone(offset) .build(), @@ -1354,11 +1354,11 @@ mod tests { async fn datetime_with_tz_clean_ambiguous_time_prefer_earliest() { let offset = Tz::America__New_York; let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .timezone(offset) .prefer_latest(false) @@ -1377,11 +1377,11 @@ mod tests { async fn datetime_with_tz_clean_ambiguous_time_prefer_latest() { let offset = Tz::America__New_York; let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .timezone(offset) .prefer_latest(true) @@ -1400,11 +1400,11 @@ mod tests { async fn datetime_with_tz_clean_ambiguous_time_unhandled() { let offset = Tz::America__New_York; let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .timezone(offset) .build(), @@ -1424,11 +1424,11 @@ mod tests { async fn datetime_with_tz_clean_non_existent_local_time() { let offset = Tz::America__New_York; let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .timezone(offset) .build(), @@ -1450,11 +1450,11 @@ mod tests { let min_dt = DateTime::parse_from_str("2025-05-27T10:00:00 +0000", "%Y-%m-%dT%H:%M:%S %z").unwrap(); let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .min(min_dt) .build(), @@ -1475,11 +1475,11 @@ mod tests { let max_dt = DateTime::parse_from_str("2025-05-27T10:00:00 +0000", "%Y-%m-%dT%H:%M:%S %z").unwrap(); let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder() .max(max_dt) .build(), @@ -1498,11 +1498,11 @@ mod tests { #[cot::test] async fn datetime_with_tz_clean_invalid_format() { let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder().build(), ); field @@ -1517,11 +1517,11 @@ mod tests { #[cot::test] async fn datetime_with_tz_clean_required() { let mut field = DateTimeWithTimezoneField::with_options( - FormFieldOptions { - id: "dt".into(), - name: "dt".into(), - required: true, - }, + FormFieldOptions::builder() + .id("dt".into()) + .name("dt".into()) + .required(true) + .build(), DateTimeWithTimezoneFieldOptions::builder().build(), ); @@ -1533,11 +1533,11 @@ mod tests { #[test] fn time_field_render() { let field = TimeField::with_options( - FormFieldOptions { - id: "time".into(), - name: "time".into(), - required: true, - }, + FormFieldOptions::builder() + .id("time".into()) + .name("time".into()) + .required(true) + .build(), TimeFieldOptions::builder() .min(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()) .max(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()) @@ -1557,11 +1557,11 @@ mod tests { #[cot::test] async fn time_field_clean_valid() { let mut field = TimeField::with_options( - FormFieldOptions { - id: "t".into(), - name: "t".into(), - required: true, - }, + FormFieldOptions::builder() + .id("t".into()) + .name("t".into()) + .required(true) + .build(), TimeFieldOptions::builder() .min(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()) .max(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()) @@ -1580,11 +1580,11 @@ mod tests { #[cot::test] async fn time_field_clean_below_min() { let mut field = TimeField::with_options( - FormFieldOptions { - id: "t".into(), - name: "t".into(), - required: true, - }, + FormFieldOptions::builder() + .id("t".into()) + .name("t".into()) + .required(true) + .build(), TimeFieldOptions::builder() .min(NaiveTime::parse_from_str("09:00:00", "%H:%M:%S").unwrap()) .step(Step::Value(Duration::seconds(60))) @@ -1607,11 +1607,11 @@ mod tests { #[cot::test] async fn time_field_clean_above_max() { let mut field = TimeField::with_options( - FormFieldOptions { - id: "t".into(), - name: "t".into(), - required: true, - }, + FormFieldOptions::builder() + .id("t".into()) + .name("t".into()) + .required(true) + .build(), TimeFieldOptions::builder() .max(NaiveTime::parse_from_str("17:00:00", "%H:%M:%S").unwrap()) .step(Step::Value(Duration::seconds(60))) @@ -1635,11 +1635,11 @@ mod tests { #[test] fn date_field_render() { let field = DateField::with_options( - FormFieldOptions { - id: "d".into(), - name: "d".into(), - required: true, - }, + FormFieldOptions::builder() + .id("d".into()) + .name("d".into()) + .required(true) + .build(), DateFieldOptions::builder() .min(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) .max(NaiveDate::parse_from_str("2025-05-28", "%Y-%m-%d").unwrap()) @@ -1656,11 +1656,11 @@ mod tests { #[cot::test] async fn date_field_clean_valid() { let mut field = DateField::with_options( - FormFieldOptions { - id: "d".into(), - name: "d".into(), - required: true, - }, + FormFieldOptions::builder() + .id("d".into()) + .name("d".into()) + .required(true) + .build(), DateFieldOptions::builder() .min(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) .max(NaiveDate::parse_from_str("2025-05-28", "%Y-%m-%d").unwrap()) @@ -1678,11 +1678,11 @@ mod tests { #[cot::test] async fn date_field_clean_below_min() { let mut field = DateField::with_options( - FormFieldOptions { - id: "d".into(), - name: "d".into(), - required: true, - }, + FormFieldOptions::builder() + .id("d".into()) + .name("d".into()) + .required(true) + .build(), DateFieldOptions::builder() .min(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) .step(Step::Value(Duration::days(1))) @@ -1702,11 +1702,11 @@ mod tests { #[cot::test] async fn date_field_clean_above_max() { let mut field = DateField::with_options( - FormFieldOptions { - id: "d".into(), - name: "d".into(), - required: true, - }, + FormFieldOptions::builder() + .id("d".into()) + .name("d".into()) + .required(true) + .build(), DateFieldOptions::builder() .max(NaiveDate::parse_from_str("2025-05-27", "%Y-%m-%d").unwrap()) .step(Step::Value(Duration::days(1))) diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index 3dedb9779..1a7c78bc0 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -171,11 +171,11 @@ mod tests { #[test] fn file_field_render() { let field = FileField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".into()) + .name("test".into()) + .required(true) + .build(), FileFieldOptions::builder() .accept(vec!["image/*".to_string(), ".pdf".to_string()]) .capture(Capture::Environment) @@ -193,11 +193,11 @@ mod tests { #[test] fn file_field_render_no_accept() { let field = FileField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".into()) + .name("test".into()) + .required(true) + .build(), FileFieldOptions::builder().capture(Capture::User).build(), ); @@ -212,11 +212,11 @@ mod tests { #[cot::test] async fn file_field_clean_value() { let mut field = FileField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".into()) + .name("test".into()) + .required(true) + .build(), FileFieldOptions::builder().build(), ); @@ -247,11 +247,11 @@ mod tests { #[cot::test] async fn file_field_clean_required() { let field = FileField::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".into()) + .name("test".into()) + .required(true) + .build(), FileFieldOptions::builder().build(), ); diff --git a/cot/src/form/fields/select.rs b/cot/src/form/fields/select.rs index 9528b855f..d16c61947 100644 --- a/cot/src/form/fields/select.rs +++ b/cot/src/form/fields/select.rs @@ -681,11 +681,11 @@ mod tests { #[test] fn select_field_render_default() { let field = SelectField::::with_options( - FormFieldOptions { - id: "test_select".to_owned(), - name: "test_select".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test_select".to_owned()) + .name("test_select".to_owned()) + .required(false) + .build(), SelectFieldOptions::default(), ); let html = field.to_string(); @@ -706,11 +706,11 @@ mod tests { #[test] fn select_field_render_required() { let field = SelectField::::with_options( - FormFieldOptions { - id: "test_select".to_owned(), - name: "test_select".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test_select".to_owned()) + .name("test_select".to_owned()) + .required(true) + .build(), SelectFieldOptions::default(), ); let html = field.to_string(); @@ -722,11 +722,11 @@ mod tests { #[test] fn select_field_render_custom_none_option() { let field = SelectField::::with_options( - FormFieldOptions { - id: "test_select".to_owned(), - name: "test_select".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test_select".to_owned()) + .name("test_select".to_owned()) + .required(false) + .build(), SelectFieldOptions::builder() .none_option("Please select...".to_owned()) .build(), @@ -740,11 +740,11 @@ mod tests { #[test] fn select_field_render_custom_choices() { let field = SelectField::::with_options( - FormFieldOptions { - id: "test_select".to_owned(), - name: "test_select".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test_select".to_owned()) + .name("test_select".to_owned()) + .required(false) + .build(), SelectFieldOptions::builder() .choices(vec![TestChoice::Option1, TestChoice::Option3]) .build(), @@ -759,11 +759,11 @@ mod tests { #[cot::test] async fn select_field_with_value() { let mut field = SelectField::::with_options( - FormFieldOptions { - id: "test_select".to_owned(), - name: "test_select".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test_select".to_owned()) + .name("test_select".to_owned()) + .required(false) + .build(), SelectFieldOptions::default(), ); @@ -779,11 +779,11 @@ mod tests { #[test] fn select_multiple_field_render_default() { let field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "test_multi".to_owned(), - name: "test_multi".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test_multi".to_owned()) + .name("tes_multi".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); let html = field.to_string(); @@ -801,11 +801,11 @@ mod tests { #[test] fn select_multiple_field_render_with_size() { let field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "test_multi".to_owned(), - name: "test_multi".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test_multi".to_owned()) + .name("tes_multi".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::builder().size(5).build(), ); let html = field.to_string(); @@ -816,11 +816,11 @@ mod tests { #[test] fn select_multiple_field_render_required() { let field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "test_multi".to_owned(), - name: "test_multi".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test_multi".to_owned()) + .name("tes_multi".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); let html = field.to_string(); @@ -831,11 +831,11 @@ mod tests { #[cot::test] async fn select_multiple_field_with_values() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "test_multi".to_owned(), - name: "test_multi".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test_multi".to_owned()) + .name("tes_multi".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -882,11 +882,11 @@ mod tests { #[test] fn check_required_multiple_empty() { let field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -897,11 +897,11 @@ mod tests { #[cot::test] async fn check_required_multiple_with_values() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -920,11 +920,11 @@ mod tests { #[cot::test] async fn select_multiple_field_values_iterator() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "test".to_owned(), - name: "test".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("test".to_owned()) + .name("test".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -953,11 +953,11 @@ mod tests { #[cot::test] async fn vec_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "choices".to_owned(), - name: "choices".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("choices".to_owned()) + .name("choices".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -977,11 +977,11 @@ mod tests { #[cot::test] async fn vec_as_form_field_required_empty() { let field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "choices".to_owned(), - name: "choices".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("choices".to_owned()) + .name("choices".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -992,11 +992,11 @@ mod tests { #[cot::test] async fn vec_as_form_field_invalid_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "choices".to_owned(), - name: "choices".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("choices".to_owned()) + .name("choices".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1025,11 +1025,11 @@ mod tests { #[cot::test] async fn vec_deque_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "choices".to_owned(), - name: "choices".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("choices".to_owned()) + .name("choices".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1050,11 +1050,11 @@ mod tests { #[cot::test] async fn linked_list_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "choices".to_owned(), - name: "choices".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("choices".to_owned()) + .name("choices".to_owned()) + .required(false) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1071,11 +1071,11 @@ mod tests { #[cot::test] async fn hash_set_as_form_field_clean_value() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "choices".to_owned(), - name: "choices".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("choices".to_owned()) + .name("choices".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1101,11 +1101,11 @@ mod tests { #[cot::test] async fn index_set_as_form_field_preserves_order() { let mut field = SelectMultipleField::::with_options( - FormFieldOptions { - id: "choices".to_owned(), - name: "choices".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("choices".to_owned()) + .name("choices".to_owned()) + .required(true) + .build(), SelectMultipleFieldOptions::default(), ); @@ -1142,11 +1142,11 @@ mod tests { #[test] fn select_as_form_field_render() { let field = SelectField::::with_options( - FormFieldOptions { - id: "status".to_owned(), - name: "status".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("status".to_owned()) + .name("status".to_owned()) + .required(false) + .build(), SelectFieldOptions::default(), ); let html = field.to_string(); @@ -1165,11 +1165,11 @@ mod tests { #[cot::test] async fn select_as_form_field_clean_value_valid() { let mut field = SelectField::::with_options( - FormFieldOptions { - id: "status".to_owned(), - name: "status".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("status".to_owned()) + .name("status".to_owned()) + .required(true) + .build(), SelectFieldOptions::default(), ); @@ -1185,11 +1185,11 @@ mod tests { #[cot::test] async fn select_as_form_field_clean_value_required_empty() { let mut field = SelectField::::with_options( - FormFieldOptions { - id: "status".to_owned(), - name: "status".to_owned(), - required: true, - }, + FormFieldOptions::builder() + .id("status".to_owned()) + .name("status".to_owned()) + .required(true) + .build(), SelectFieldOptions::default(), ); @@ -1202,11 +1202,11 @@ mod tests { #[cot::test] async fn select_as_form_field_clean_value_invalid() { let mut field = SelectField::::with_options( - FormFieldOptions { - id: "status".to_owned(), - name: "status".to_owned(), - required: false, - }, + FormFieldOptions::builder() + .id("status".to_owned()) + .name("status".to_owned()) + .required(false) + .build(), SelectFieldOptions::default(), ); From bbd2f42d0481c9668c59dbafdf8998442f5d212f Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 10 Jul 2026 14:10:34 +0000 Subject: [PATCH 18/19] clippy fix --- cot-cli/src/migration_generator.rs | 22 ++++++++-------------- cot/src/auth.rs | 2 +- cot/src/openapi.rs | 4 ++-- cot/src/session/store/file.rs | 2 +- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/cot-cli/src/migration_generator.rs b/cot-cli/src/migration_generator.rs index 2053453ea..b5010bd37 100644 --- a/cot-cli/src/migration_generator.rs +++ b/cot-cli/src/migration_generator.rs @@ -770,7 +770,7 @@ impl MigrationOperationGenerator { fn make_remove_model_operation(migration_model: &ModelInSource) -> DynOperation { print_status_msg( StatusType::Removing, - &format!("Model '{}'", &migration_model.model.name), + &format!("Model '{}'", migration_model.model.name), ); let op = DynOperation::RemoveModel { @@ -781,7 +781,7 @@ impl MigrationOperationGenerator { print_status_msg( StatusType::Removed, - &format!("Model '{}'", &migration_model.model.name), + &format!("Model '{}'", migration_model.model.name), ); op @@ -791,10 +791,7 @@ impl MigrationOperationGenerator { fn make_add_field_operation(app_model: &ModelInSource, field: &Field) -> DynOperation { print_status_msg( StatusType::Adding, - &format!( - "Field '{}' to Model '{}'", - &field.name, app_model.model.name - ), + &format!("Field '{}' to Model '{}'", field.name, app_model.model.name), ); let op = DynOperation::AddField { @@ -805,10 +802,7 @@ impl MigrationOperationGenerator { print_status_msg( StatusType::Added, - &format!( - "Field '{}' to Model '{}'", - &field.name, app_model.model.name - ), + &format!("Field '{}' to Model '{}'", field.name, app_model.model.name), ); op @@ -828,7 +822,7 @@ impl MigrationOperationGenerator { StatusType::Modifying, &format!( "Field '{}' from Model '{}'", - &migration_field.name, migration_model.model.name + migration_field.name, migration_model.model.name ), ); @@ -839,7 +833,7 @@ impl MigrationOperationGenerator { StatusType::Modified, &format!( "Field '{}' from Model '{}'", - &migration_field.name, migration_model.model.name + migration_field.name, migration_model.model.name ), ); } @@ -853,7 +847,7 @@ impl MigrationOperationGenerator { StatusType::Removing, &format!( "Field '{}' from Model '{}'", - &migration_field.name, migration_model.model.name + migration_field.name, migration_model.model.name ), ); @@ -867,7 +861,7 @@ impl MigrationOperationGenerator { StatusType::Removed, &format!( "Field '{}' from Model '{}'", - &migration_field.name, migration_model.model.name + migration_field.name, migration_model.model.name ), ); diff --git a/cot/src/auth.rs b/cot/src/auth.rs index 550bf1d2b..d69d3c335 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -1307,7 +1307,7 @@ mod tests { let id_2 = session.id(); assert!(id_2.is_some()); - assert!(id_1 != id_2); + assert_ne!(id_1, id_2); } /// Test that the user is logged out when there is an invalid user ID in the diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index acc58bc23..f2b41c6b6 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -1360,7 +1360,7 @@ mod tests { panic!("Expected object schema"); } } else { - panic!("Expected request body: {:?}", &operation.request_body); + panic!("Expected request body: {:?}", operation.request_body); } } @@ -1430,7 +1430,7 @@ mod tests { panic!("Expected object schema"); } } else { - panic!("Expected request body: {:?}", &operation.request_body); + panic!("Expected request body: {:?}", operation.request_body); } } diff --git a/cot/src/session/store/file.rs b/cot/src/session/store/file.rs index 23af45dc3..5b45f4cbf 100644 --- a/cot/src/session/store/file.rs +++ b/cot/src/session/store/file.rs @@ -194,7 +194,7 @@ impl SessionStore for FileStore { if let Err(e) = res && e.kind() != io::ErrorKind::NotFound { - return Err(FileStoreError::Io(Box::new(e)))?; + Err(FileStoreError::Io(Box::new(e)))?; } Ok(()) } From f2645cdedcaa0deee2abc3ea8d2de607dfb8c84c Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 10 Jul 2026 14:25:51 +0000 Subject: [PATCH 19/19] fix some typos --- cot/src/form/fields.rs | 4 ++-- cot/src/form/fields/select.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index b0623a541..b30c73c0e 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -1507,8 +1507,8 @@ mod tests { .required(true) .build(), EmailFieldOptions::builder() - .max_length(50) - .min_length(10) + .max_length(10) + .min_length(5) .build(), ); diff --git a/cot/src/form/fields/select.rs b/cot/src/form/fields/select.rs index d16c61947..a5922f8ba 100644 --- a/cot/src/form/fields/select.rs +++ b/cot/src/form/fields/select.rs @@ -781,7 +781,7 @@ mod tests { let field = SelectMultipleField::::with_options( FormFieldOptions::builder() .id("test_multi".to_owned()) - .name("tes_multi".to_owned()) + .name("test_multi".to_owned()) .required(false) .build(), SelectMultipleFieldOptions::default(), @@ -803,7 +803,7 @@ mod tests { let field = SelectMultipleField::::with_options( FormFieldOptions::builder() .id("test_multi".to_owned()) - .name("tes_multi".to_owned()) + .name("test_multi".to_owned()) .required(false) .build(), SelectMultipleFieldOptions::builder().size(5).build(), @@ -818,7 +818,7 @@ mod tests { let field = SelectMultipleField::::with_options( FormFieldOptions::builder() .id("test_multi".to_owned()) - .name("tes_multi".to_owned()) + .name("test_multi".to_owned()) .required(true) .build(), SelectMultipleFieldOptions::default(), @@ -833,7 +833,7 @@ mod tests { let mut field = SelectMultipleField::::with_options( FormFieldOptions::builder() .id("test_multi".to_owned()) - .name("tes_multi".to_owned()) + .name("test_multi".to_owned()) .required(false) .build(), SelectMultipleFieldOptions::default(),