cot/form/
fields.rs

1use std::borrow::Cow;
2use std::fmt::{Debug, Display, Formatter};
3use std::num::{
4    NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8,
5    NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize,
6};
7
8#[cfg(feature = "db")]
9use cot::db::Auto;
10use rinja::filters::HtmlSafe;
11
12use crate::auth::{Password, PasswordHash};
13#[cfg(feature = "db")]
14use crate::db::LimitedString;
15use crate::form::{AsFormField, FormField, FormFieldOptions, FormFieldValidationError};
16use crate::html::HtmlTag;
17
18macro_rules! impl_form_field {
19    ($field_type_name:ident, $field_options_type_name:ident, $purpose:literal $(, $generic_param:ident $(: $generic_param_bound:ident $(+ $generic_param_bound_more:ident)*)?)?) => {
20        #[derive(Debug)]
21        #[doc = concat!("A form field for ", $purpose, ".")]
22        pub struct $field_type_name $(<$generic_param>)? {
23            options: FormFieldOptions,
24            custom_options: $field_options_type_name $(<$generic_param>)?,
25            value: Option<String>,
26        }
27
28        impl $(<$generic_param $(: $generic_param_bound $(+ $generic_param_bound_more)* )?>)? FormField for $field_type_name $(<$generic_param>)? {
29            type CustomOptions = $field_options_type_name $(<$generic_param>)?;
30
31            fn with_options(
32                options: FormFieldOptions,
33                custom_options: Self::CustomOptions,
34            ) -> Self {
35                Self {
36                    options,
37                    custom_options,
38                    value: None,
39                }
40            }
41
42            fn options(&self) -> &FormFieldOptions {
43                &self.options
44            }
45
46            fn value(&self) -> Option<&str> {
47                self.value.as_deref()
48            }
49
50            fn set_value(&mut self, value: Cow<'_, str>) {
51                self.value = Some(value.into_owned());
52            }
53        }
54    };
55}
56
57impl_form_field!(StringField, StringFieldOptions, "a string");
58
59/// Custom options for a [`StringField`].
60#[derive(Debug, Default, Copy, Clone)]
61pub struct StringFieldOptions {
62    /// The maximum length of the field. Used to set the `maxlength` attribute
63    /// in the HTML input element.
64    pub max_length: Option<u32>,
65}
66
67impl Display for StringField {
68    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69        let mut tag = HtmlTag::input("text");
70        tag.attr("name", self.id());
71        tag.attr("id", self.id());
72        if self.options.required {
73            tag.bool_attr("required");
74        }
75        if let Some(max_length) = self.custom_options.max_length {
76            tag.attr("maxlength", &max_length.to_string());
77        }
78        if let Some(value) = &self.value {
79            tag.attr("value", value);
80        }
81
82        write!(f, "{}", tag.render())
83    }
84}
85
86impl HtmlSafe for StringField {}
87
88impl AsFormField for String {
89    type Type = StringField;
90
91    fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
92        let value = check_required(field)?;
93
94        if let Some(max_length) = field.custom_options.max_length {
95            if value.len() > max_length as usize {
96                return Err(FormFieldValidationError::maximum_length_exceeded(
97                    max_length,
98                ));
99            }
100        }
101        Ok(value.to_owned())
102    }
103
104    fn to_field_value(&self) -> String {
105        self.to_owned()
106    }
107}
108
109#[cfg(feature = "db")]
110impl<const LEN: u32> AsFormField for LimitedString<LEN> {
111    type Type = StringField;
112
113    fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
114        let value = check_required(field)?;
115
116        if value.len() > LEN as usize {
117            return Err(FormFieldValidationError::maximum_length_exceeded(LEN));
118        }
119        Ok(LimitedString::new(value.to_owned()).expect("length has already been checked"))
120    }
121
122    fn to_field_value(&self) -> String {
123        self.to_string()
124    }
125}
126
127impl_form_field!(PasswordField, PasswordFieldOptions, "a password");
128
129/// Custom options for a [`PasswordField`].
130#[derive(Debug, Default, Copy, Clone)]
131pub struct PasswordFieldOptions {
132    /// The maximum length of the field. Used to set the `maxlength` attribute
133    /// in the HTML input element.
134    pub max_length: Option<u32>,
135}
136
137impl Display for PasswordField {
138    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
139        let mut tag = HtmlTag::input("password");
140        tag.attr("name", self.id());
141        tag.attr("id", self.id());
142        if self.options.required {
143            tag.bool_attr("required");
144        }
145        if let Some(max_length) = self.custom_options.max_length {
146            tag.attr("maxlength", &max_length.to_string());
147        }
148        // we don't set the value attribute for password fields
149        // to avoid leaking the password in the HTML
150
151        write!(f, "{}", tag.render())
152    }
153}
154
155impl HtmlSafe for PasswordField {}
156
157impl AsFormField for Password {
158    type Type = PasswordField;
159
160    fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
161        let value = check_required(field)?;
162
163        if let Some(max_length) = field.custom_options.max_length {
164            if value.len() > max_length as usize {
165                return Err(FormFieldValidationError::maximum_length_exceeded(
166                    max_length,
167                ));
168            }
169        }
170
171        Ok(Password::new(value))
172    }
173
174    fn to_field_value(&self) -> String {
175        self.as_str().to_owned()
176    }
177}
178
179impl AsFormField for PasswordHash {
180    type Type = PasswordField;
181
182    fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
183        let value = check_required(field)?;
184
185        if let Some(max_length) = field.custom_options.max_length {
186            if value.len() > max_length as usize {
187                return Err(FormFieldValidationError::maximum_length_exceeded(
188                    max_length,
189                ));
190            }
191        }
192
193        Ok(PasswordHash::from_password(&Password::new(value)))
194    }
195
196    fn to_field_value(&self) -> String {
197        // cannot return the original password
198        String::new()
199    }
200}
201
202impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer);
203
204/// Custom options for a `IntegerField`.
205#[derive(Debug, Copy, Clone)]
206pub struct IntegerFieldOptions<T> {
207    /// The minimum value of the field. Used to set the `min` attribute in the
208    /// HTML input element.
209    pub min: Option<T>,
210    /// The maximum value of the field. Used to set the `max` attribute in the
211    /// HTML input element.
212    pub max: Option<T>,
213}
214
215impl<T: Integer> Default for IntegerFieldOptions<T> {
216    fn default() -> Self {
217        Self {
218            min: T::MIN,
219            max: T::MAX,
220        }
221    }
222}
223
224impl<T: Integer> Display for IntegerField<T> {
225    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
226        let mut tag = HtmlTag::input("number");
227        tag.attr("name", self.id());
228        tag.attr("id", self.id());
229        if self.options.required {
230            tag.bool_attr("required");
231        }
232        if let Some(min) = &self.custom_options.min {
233            tag.attr("min", &min.to_string());
234        }
235        if let Some(max) = &self.custom_options.max {
236            tag.attr("max", &max.to_string());
237        }
238        if let Some(value) = &self.value {
239            tag.attr("value", value);
240        }
241
242        write!(f, "{}", tag.render())
243    }
244}
245
246impl<T: Integer> HtmlSafe for IntegerField<T> {}
247
248/// A trait for numerical types that optionally have minimum and maximum values.
249///
250/// # Examples
251///
252/// ```
253/// use cot::form::fields::Integer;
254///
255/// assert_eq!(<i8 as Integer>::MIN, Some(-128));
256/// assert_eq!(<i8 as Integer>::MAX, Some(127));
257/// ```
258pub trait Integer: Sized + ToString {
259    /// The minimum value of the type.
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use cot::form::fields::Integer;
265    ///
266    /// assert_eq!(<i8 as Integer>::MIN, Some(-128));
267    /// ```
268    const MIN: Option<Self>;
269    /// The maximum value of the type.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use cot::form::fields::Integer;
275    ///
276    /// assert_eq!(<i8 as Integer>::MAX, Some(127));
277    /// ```
278    const MAX: Option<Self>;
279}
280
281macro_rules! impl_integer {
282    ($type:ty) => {
283        impl Integer for $type {
284            const MAX: Option<Self> = Some(Self::MAX);
285            const MIN: Option<Self> = Some(Self::MIN);
286        }
287    };
288}
289
290impl_integer!(i8);
291impl_integer!(i16);
292impl_integer!(i32);
293impl_integer!(i64);
294impl_integer!(i128);
295impl_integer!(isize);
296impl_integer!(u8);
297impl_integer!(u16);
298impl_integer!(u32);
299impl_integer!(u64);
300impl_integer!(u128);
301impl_integer!(usize);
302impl_integer!(NonZeroI8);
303impl_integer!(NonZeroI16);
304impl_integer!(NonZeroI32);
305impl_integer!(NonZeroI64);
306impl_integer!(NonZeroI128);
307impl_integer!(NonZeroIsize);
308impl_integer!(NonZeroU8);
309impl_integer!(NonZeroU16);
310impl_integer!(NonZeroU32);
311impl_integer!(NonZeroU64);
312impl_integer!(NonZeroU128);
313impl_integer!(NonZeroUsize);
314
315macro_rules! impl_integer_as_form_field {
316    ($type:ty) => {
317        impl AsFormField for $type {
318            type Type = IntegerField<$type>;
319
320            fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
321                let value = check_required(field)?;
322
323                value
324                    .parse()
325                    .map_err(|_| FormFieldValidationError::invalid_value(value))
326            }
327
328            fn to_field_value(&self) -> String {
329                self.to_string()
330            }
331        }
332    };
333}
334
335impl_integer_as_form_field!(i8);
336impl_integer_as_form_field!(i16);
337impl_integer_as_form_field!(i32);
338impl_integer_as_form_field!(i64);
339impl_integer_as_form_field!(i128);
340impl_integer_as_form_field!(isize);
341impl_integer_as_form_field!(u8);
342impl_integer_as_form_field!(u16);
343impl_integer_as_form_field!(u32);
344impl_integer_as_form_field!(u64);
345impl_integer_as_form_field!(u128);
346impl_integer_as_form_field!(usize);
347impl_integer_as_form_field!(NonZeroI8);
348impl_integer_as_form_field!(NonZeroI16);
349impl_integer_as_form_field!(NonZeroI32);
350impl_integer_as_form_field!(NonZeroI64);
351impl_integer_as_form_field!(NonZeroI128);
352impl_integer_as_form_field!(NonZeroIsize);
353impl_integer_as_form_field!(NonZeroU8);
354impl_integer_as_form_field!(NonZeroU16);
355impl_integer_as_form_field!(NonZeroU32);
356impl_integer_as_form_field!(NonZeroU64);
357impl_integer_as_form_field!(NonZeroU128);
358impl_integer_as_form_field!(NonZeroUsize);
359
360impl_form_field!(BoolField, BoolFieldOptions, "a boolean");
361
362/// Custom options for a `BoolField`.
363#[derive(Debug, Default, Copy, Clone)]
364pub struct BoolFieldOptions {
365    /// If `true`, the field must be checked to be considered valid.
366    pub must_be_true: Option<bool>,
367}
368
369impl Display for BoolField {
370    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
371        let mut bool_input = HtmlTag::input("checkbox");
372        bool_input.attr("name", self.id());
373        bool_input.attr("id", self.id());
374        bool_input.attr("value", "1");
375
376        if self.custom_options.must_be_true.unwrap_or(false) {
377            bool_input.bool_attr("required");
378            return write!(f, "{}", bool_input.render());
379        }
380
381        if let Some(value) = &self.value {
382            if value == "1" {
383                bool_input.bool_attr("checked");
384            }
385        }
386
387        // Web browsers don't send anything when a checkbox is unchecked, so we
388        // need to add a hidden input to send a "false" value.
389        let mut hidden_input = HtmlTag::input("hidden");
390        hidden_input.attr("name", self.id());
391        hidden_input.attr("value", "0");
392        let hidden = hidden_input.render();
393
394        let checkbox = bool_input.render();
395        write!(f, "{}{}", hidden.as_str(), checkbox.as_str())
396    }
397}
398
399impl HtmlSafe for BoolField {}
400
401/// Implementation of `AsFormField` for `bool`.
402///
403/// This implementation converts the string values "true", "on", and "1" to
404/// `true`, and "false", "  off", and "0" to `false`. It returns an error if the
405/// value is not one of these strings. If the field is required to be `true` by
406/// the field's options, it will return an error if the value is `false`.
407impl AsFormField for bool {
408    type Type = BoolField;
409
410    fn new_field(
411        mut options: FormFieldOptions,
412        custom_options: <Self::Type as FormField>::CustomOptions,
413    ) -> Self::Type {
414        options.required = false;
415        Self::Type::with_options(options, custom_options)
416    }
417
418    fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
419        let value = check_required(field)?;
420        let value = if ["true", "on", "1"].contains(&value) {
421            true
422        } else if ["false", "off", "0"].contains(&value) {
423            false
424        } else {
425            return Err(FormFieldValidationError::invalid_value(value));
426        };
427
428        if field.custom_options.must_be_true.unwrap_or(false) && !value {
429            return Err(FormFieldValidationError::BooleanRequiredToBeTrue);
430        }
431        Ok(value.to_owned())
432    }
433
434    fn to_field_value(&self) -> String {
435        String::from(if *self { "1" } else { "0" })
436    }
437}
438
439impl<T: AsFormField> AsFormField for Option<T> {
440    type Type = T::Type;
441
442    fn new_field(
443        mut options: FormFieldOptions,
444        custom_options: <Self::Type as FormField>::CustomOptions,
445    ) -> Self::Type {
446        options.required = false;
447        Self::Type::with_options(options, custom_options)
448    }
449
450    fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
451        let value = T::clean_value(field);
452        match value {
453            Ok(value) => Ok(Some(value)),
454            Err(FormFieldValidationError::Required) => Ok(None),
455            Err(error) => Err(error),
456        }
457    }
458
459    fn to_field_value(&self) -> String {
460        match self {
461            Some(value) => value.to_field_value(),
462            None => String::new(),
463        }
464    }
465}
466
467#[cfg(feature = "db")]
468impl<T: AsFormField> AsFormField for Auto<T> {
469    type Type = T::Type;
470
471    fn new_field(
472        mut options: FormFieldOptions,
473        custom_options: <Self::Type as FormField>::CustomOptions,
474    ) -> Self::Type {
475        options.required = false;
476        Self::Type::with_options(options, custom_options)
477    }
478
479    fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError>
480    where
481        Self: Sized,
482    {
483        let value = T::clean_value(field);
484        match value {
485            Ok(value) => Ok(Auto::fixed(value)),
486            Err(FormFieldValidationError::Required) => Ok(Auto::auto()),
487            Err(error) => Err(error),
488        }
489    }
490
491    fn to_field_value(&self) -> String {
492        match self {
493            Auto::Fixed(value) => value.to_field_value(),
494            Auto::Auto => String::new(),
495        }
496    }
497}
498
499fn check_required<T: FormField>(field: &T) -> Result<&str, FormFieldValidationError> {
500    if let Some(value) = field.value() {
501        if value.is_empty() {
502            Err(FormFieldValidationError::Required)
503        } else {
504            Ok(value)
505        }
506    } else {
507        Err(FormFieldValidationError::Required)
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use std::borrow::Cow;
514
515    use super::*;
516
517    #[test]
518    fn string_field_render() {
519        let field = StringField::with_options(
520            FormFieldOptions {
521                id: "test".to_owned(),
522                name: "test".to_owned(),
523                required: true,
524            },
525            StringFieldOptions {
526                max_length: Some(10),
527            },
528        );
529        let html = field.to_string();
530        assert!(html.contains("type=\"text\""));
531        assert!(html.contains("required"));
532        assert!(html.contains("maxlength=\"10\""));
533    }
534
535    #[test]
536    fn password_field_render() {
537        let field = PasswordField::with_options(
538            FormFieldOptions {
539                id: "test".to_owned(),
540                name: "test".to_owned(),
541                required: true,
542            },
543            PasswordFieldOptions {
544                max_length: Some(10),
545            },
546        );
547        let html = field.to_string();
548        assert!(html.contains("type=\"password\""));
549        assert!(html.contains("required"));
550        assert!(html.contains("maxlength=\"10\""));
551    }
552
553    #[test]
554    fn integer_field_render() {
555        let field = IntegerField::<i32>::with_options(
556            FormFieldOptions {
557                id: "test".to_owned(),
558                name: "test".to_owned(),
559                required: true,
560            },
561            IntegerFieldOptions {
562                min: Some(1),
563                max: Some(10),
564            },
565        );
566        let html = field.to_string();
567        assert!(html.contains("type=\"number\""));
568        assert!(html.contains("required"));
569        assert!(html.contains("min=\"1\""));
570        assert!(html.contains("max=\"10\""));
571    }
572
573    #[test]
574    fn bool_field_render() {
575        let field = BoolField::with_options(
576            FormFieldOptions {
577                id: "test".to_owned(),
578                name: "test".to_owned(),
579                required: true,
580            },
581            BoolFieldOptions {
582                must_be_true: Some(false),
583            },
584        );
585        let html = field.to_string();
586        assert!(html.contains("type=\"checkbox\""));
587        assert!(html.contains("type=\"hidden\""));
588        assert!(!html.contains("required"));
589    }
590
591    #[test]
592    fn bool_field_render_must_be_true() {
593        let field = BoolField::with_options(
594            FormFieldOptions {
595                id: "test".to_owned(),
596                name: "test".to_owned(),
597                required: true,
598            },
599            BoolFieldOptions {
600                must_be_true: Some(true),
601            },
602        );
603        let html = field.to_string();
604        assert!(html.contains("type=\"checkbox\""));
605        assert!(!html.contains("type=\"hidden\""));
606        assert!(html.contains("required"));
607    }
608
609    #[test]
610    fn string_field_clean_value() {
611        let mut field = StringField::with_options(
612            FormFieldOptions {
613                id: "test".to_owned(),
614                name: "test".to_owned(),
615                required: true,
616            },
617            StringFieldOptions {
618                max_length: Some(10),
619            },
620        );
621        field.set_value(Cow::Borrowed("test"));
622        let value = String::clean_value(&field).unwrap();
623        assert_eq!(value, "test");
624    }
625
626    #[test]
627    fn string_field_clean_required() {
628        let mut field = StringField::with_options(
629            FormFieldOptions {
630                id: "test".to_owned(),
631                name: "test".to_owned(),
632                required: true,
633            },
634            StringFieldOptions {
635                max_length: Some(10),
636            },
637        );
638        field.set_value(Cow::Borrowed(""));
639        let value = String::clean_value(&field);
640        assert_eq!(value, Err(FormFieldValidationError::Required));
641    }
642
643    #[test]
644    fn password_field_clean_value() {
645        let mut field = PasswordField::with_options(
646            FormFieldOptions {
647                id: "test".to_owned(),
648                name: "test".to_owned(),
649                required: true,
650            },
651            PasswordFieldOptions {
652                max_length: Some(10),
653            },
654        );
655        field.set_value(Cow::Borrowed("password"));
656        let value = Password::clean_value(&field).unwrap();
657        assert_eq!(value.as_str(), "password");
658    }
659
660    #[test]
661    fn integer_field_clean_value() {
662        let mut field = IntegerField::<i32>::with_options(
663            FormFieldOptions {
664                id: "test".to_owned(),
665                name: "test".to_owned(),
666                required: true,
667            },
668            IntegerFieldOptions {
669                min: Some(1),
670                max: Some(10),
671            },
672        );
673        field.set_value(Cow::Borrowed("5"));
674        let value = i32::clean_value(&field).unwrap();
675        assert_eq!(value, 5);
676    }
677
678    #[test]
679    fn bool_field_clean_value() {
680        let mut field = BoolField::with_options(
681            FormFieldOptions {
682                id: "test".to_owned(),
683                name: "test".to_owned(),
684                required: true,
685            },
686            BoolFieldOptions {
687                must_be_true: Some(true),
688            },
689        );
690        field.set_value(Cow::Borrowed("true"));
691        let value = bool::clean_value(&field).unwrap();
692        assert!(value);
693    }
694}