mendes/
forms.rs

1use std::borrow::Cow;
2use std::{fmt, str};
3
4pub use mendes_macros::{form, ToField};
5use thiserror::Error;
6
7#[cfg(feature = "uploads")]
8#[cfg_attr(docsrs, doc(cfg(feature = "uploads")))]
9pub use crate::multipart::{from_form_data, File};
10
11/// A data type that knows how to generate an HTML form for itself
12///
13/// Implementations are usually generated using the `form` procedural macro attribute.
14pub trait ToForm {
15    fn to_form() -> Form;
16}
17
18/// An HTML form
19pub struct Form {
20    /// The form action (a relative URL to send the form to)
21    pub action: Option<Cow<'static, str>>,
22    /// The form data encoding type
23    pub enctype: Option<Cow<'static, str>>,
24    /// The method to use on form submission
25    pub method: Option<Cow<'static, str>>,
26    /// List of classes to set on the form element
27    pub classes: Vec<Cow<'static, str>>,
28    /// The field sets contained in this form
29    pub sets: Vec<FieldSet>,
30}
31
32impl Form {
33    // This should only be used by procedural macros.
34    #[doc(hidden)]
35    pub fn prepare(mut self) -> Self {
36        let multipart = self
37            .sets
38            .iter()
39            .flat_map(|s| &s.items)
40            .any(|i| i.multipart());
41        if multipart {
42            self.enctype = Some("multipart/form-data".into());
43        }
44        self
45    }
46
47    pub fn set<T: fmt::Display>(mut self, name: &str, value: T) -> Result<Self, Error> {
48        self.sets
49            .iter_mut()
50            .flat_map(|s| &mut s.items)
51            .try_fold((), |_, item| item.set(name, &value))
52            .map(|_| self)
53    }
54}
55
56impl fmt::Display for Form {
57    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(fmt, "<form")?;
59        if let Some(s) = &self.action {
60            write!(fmt, r#" action="{s}""#)?;
61        }
62        if let Some(s) = &self.enctype {
63            write!(fmt, r#" enctype="{s}""#)?;
64        }
65        if let Some(s) = &self.method {
66            write!(fmt, r#" method="{s}""#)?;
67        }
68        if !self.classes.is_empty() {
69            write!(fmt, r#" class=""#)?;
70            for (i, s) in self.classes.iter().enumerate() {
71                match i {
72                    0 => write!(fmt, "{s}")?,
73                    _ => write!(fmt, " {s}")?,
74                }
75            }
76            write!(fmt, "\"")?;
77        }
78        write!(fmt, ">")?;
79        for set in &self.sets {
80            write!(fmt, "{set}")?;
81        }
82        write!(fmt, "</form>")
83    }
84}
85
86pub struct FieldSet {
87    pub legend: Option<&'static str>,
88    pub items: Vec<Item>,
89}
90
91impl fmt::Display for FieldSet {
92    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(fmt, "<fieldset>")?;
94        if let Some(s) = self.legend {
95            write!(fmt, "<legend>{s}</legend>")?;
96        }
97        for item in &self.items {
98            write!(fmt, "{item}")?;
99        }
100        write!(fmt, "</fieldset>")
101    }
102}
103
104pub struct Item {
105    pub label: Option<Cow<'static, str>>,
106    pub contents: ItemContents,
107}
108
109impl Item {
110    fn set<T: fmt::Display>(&mut self, name: &str, value: &T) -> Result<(), Error> {
111        match &mut self.contents {
112            ItemContents::Single(f) => {
113                if f.name() != Some(name) {
114                    return Ok(());
115                }
116
117                match f {
118                    Field::Checkbox(f) => {
119                        let s = value.to_string();
120                        if s == "true" || s == "1" {
121                            f.checked = true;
122                            Ok(())
123                        } else if s == "false" || s == "0" {
124                            f.checked = false;
125                            Ok(())
126                        } else {
127                            Err(Error::SetInvalidBooleanValue)
128                        }
129                    }
130                    Field::Date(f) => {
131                        f.value = Some(value.to_string().into());
132                        Ok(())
133                    }
134                    Field::Email(f) => {
135                        f.value = Some(value.to_string().into());
136                        Ok(())
137                    }
138                    Field::Hidden(f) => {
139                        f.value = Some(value.to_string().into());
140                        Ok(())
141                    }
142                    Field::Number(f) => {
143                        f.value = Some(value.to_string().into());
144                        Ok(())
145                    }
146                    Field::Password(f) => {
147                        f.value = Some(value.to_string().into());
148                        Ok(())
149                    }
150                    Field::Select(f) => {
151                        let val = value.to_string();
152                        for option in &mut f.options {
153                            if option.value == val {
154                                option.selected = true;
155                                return Ok(());
156                            }
157                        }
158                        Err(Error::SetOptionNotFound)
159                    }
160                    Field::Text(f) => {
161                        f.value = Some(value.to_string().into());
162                        Ok(())
163                    }
164                    Field::File(_) | Field::Submit(_) => Err(Error::SetUnsupportedFieldType),
165                }
166            }
167            ItemContents::Multi(items) => {
168                for item in items {
169                    item.set(name, value)?;
170                }
171                Ok(())
172            }
173        }
174    }
175
176    fn multipart(&self) -> bool {
177        match &self.contents {
178            ItemContents::Single(f) => matches!(f, Field::File(_)),
179            ItemContents::Multi(items) => items.iter().any(|i| i.multipart()),
180        }
181    }
182}
183
184impl fmt::Display for Item {
185    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
186        match (&self.contents, &self.label) {
187            (ItemContents::Single(Field::Submit(_)), None) => write!(fmt, "{}", self.contents),
188            (ItemContents::Single(f), Some(l)) => write!(
189                fmt,
190                r#"<label for="{}">{}</label>{}"#,
191                f.name().unwrap(),
192                l,
193                self.contents
194            ),
195            (_, Some(l)) => write!(fmt, r#"<label>{}</label>{}"#, l, self.contents),
196            (_, None) => write!(fmt, "{}", self.contents),
197        }
198    }
199}
200
201pub enum ItemContents {
202    Single(Field),
203    Multi(Vec<Item>),
204}
205
206impl fmt::Display for ItemContents {
207    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
208        match self {
209            ItemContents::Single(f) => write!(fmt, "{f}"),
210            ItemContents::Multi(items) => {
211                write!(fmt, r#"<div class="compound-item">"#)?;
212                for item in items {
213                    write!(fmt, "{item}")?;
214                }
215                write!(fmt, "</div>")
216            }
217        }
218    }
219}
220
221pub enum Field {
222    Checkbox(Checkbox),
223    Date(Date),
224    Email(Email),
225    File(FileInput),
226    Hidden(Hidden),
227    Number(Number),
228    Password(Password),
229    Select(Select),
230    Submit(Submit),
231    Text(Text),
232}
233
234impl Field {
235    pub fn name(&self) -> Option<&str> {
236        use Field::*;
237        match self {
238            Checkbox(f) => Some(&f.name),
239            Date(f) => Some(&f.name),
240            Email(f) => Some(&f.name),
241            File(f) => Some(&f.name),
242            Hidden(f) => Some(&f.name),
243            Number(f) => Some(&f.name),
244            Password(f) => Some(&f.name),
245            Select(f) => Some(&f.name),
246            Text(f) => Some(&f.name),
247            Submit(_) => None,
248        }
249    }
250}
251
252impl fmt::Display for Field {
253    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
254        use Field::*;
255        match self {
256            Checkbox(f) => write!(fmt, "{f}"),
257            Date(f) => write!(fmt, "{f}"),
258            Email(f) => write!(fmt, "{f}"),
259            File(f) => write!(fmt, "{f}"),
260            Hidden(f) => write!(fmt, "{f}"),
261            Number(f) => write!(fmt, "{f}"),
262            Password(f) => write!(fmt, "{f}"),
263            Select(f) => write!(fmt, "{f}"),
264            Submit(f) => write!(fmt, "{f}"),
265            Text(f) => write!(fmt, "{f}"),
266        }
267    }
268}
269
270pub struct Checkbox {
271    pub name: Cow<'static, str>,
272    pub checked: bool,
273}
274
275impl fmt::Display for Checkbox {
276    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
277        write!(
278            fmt,
279            r#"<input type="checkbox" name="{}" value="true""#,
280            self.name
281        )?;
282        if self.checked {
283            write!(fmt, " checked")?;
284        }
285        write!(fmt, ">")
286    }
287}
288
289pub struct Date {
290    pub name: Cow<'static, str>,
291    pub value: Option<Cow<'static, str>>,
292}
293
294impl fmt::Display for Date {
295    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
296        write!(fmt, r#"<input type="date" name="{}""#, self.name)?;
297        if let Some(s) = &self.value {
298            write!(fmt, r#" value="{s}""#)?;
299        }
300        write!(fmt, ">")
301    }
302}
303
304pub struct Email {
305    pub name: Cow<'static, str>,
306    pub value: Option<Cow<'static, str>>,
307}
308
309impl fmt::Display for Email {
310    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
311        write!(fmt, r#"<input type="email" name="{}""#, self.name)?;
312        if let Some(s) = &self.value {
313            write!(fmt, r#" value="{s}""#)?;
314        }
315        write!(fmt, ">")
316    }
317}
318
319pub struct FileInput {
320    pub name: Cow<'static, str>,
321}
322
323impl fmt::Display for FileInput {
324    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
325        write!(fmt, r#"<input type="file" name="{}">"#, self.name)
326    }
327}
328
329pub struct Hidden {
330    pub name: Cow<'static, str>,
331    pub value: Option<Cow<'static, str>>,
332}
333
334impl Hidden {
335    fn from_params(name: Cow<'static, str>, _: &[(&str, &str)]) -> Self {
336        Self { name, value: None }
337    }
338}
339
340impl fmt::Display for Hidden {
341    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
342        write!(fmt, r#"<input type="hidden" name="{}""#, self.name)?;
343        if let Some(s) = &self.value {
344            write!(fmt, r#" value="{s}""#)?;
345        }
346        write!(fmt, ">")
347    }
348}
349
350pub struct Number {
351    pub name: Cow<'static, str>,
352    pub value: Option<Cow<'static, str>>,
353}
354
355impl fmt::Display for Number {
356    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
357        write!(fmt, r#"<input type="number" name="{}""#, self.name)?;
358        if let Some(s) = &self.value {
359            write!(fmt, r#" value="{s}""#)?;
360        }
361        write!(fmt, ">")
362    }
363}
364
365pub struct Password {
366    pub name: Cow<'static, str>,
367    pub value: Option<Cow<'static, str>>,
368}
369
370impl fmt::Display for Password {
371    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
372        write!(fmt, r#"<input type="password" name="{}""#, self.name)?;
373        if let Some(s) = &self.value {
374            write!(fmt, r#" value="{s}""#)?;
375        }
376        write!(fmt, ">")
377    }
378}
379
380pub struct Select {
381    pub name: Cow<'static, str>,
382    pub options: Vec<SelectOption>,
383}
384
385impl fmt::Display for Select {
386    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
387        write!(fmt, r#"<select name="{}">"#, &self.name)?;
388        for opt in &self.options {
389            write!(fmt, "{opt}")?;
390        }
391        write!(fmt, "</select>")
392    }
393}
394
395pub struct SelectOption {
396    pub label: Cow<'static, str>,
397    pub value: Cow<'static, str>,
398    pub disabled: bool,
399    pub selected: bool,
400}
401
402impl fmt::Display for SelectOption {
403    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
404        write!(fmt, r#"<option value="{}""#, self.value)?;
405        if self.disabled {
406            write!(fmt, " disabled")?;
407        }
408        if self.selected {
409            write!(fmt, " selected")?;
410        }
411        write!(fmt, ">{}</option>", self.label)
412    }
413}
414
415pub struct Submit {
416    pub value: Option<Cow<'static, str>>,
417}
418
419impl fmt::Display for Submit {
420    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
421        write!(fmt, r#"<input type="submit""#)?;
422        if let Some(s) = &self.value {
423            write!(fmt, r#" value="{s}""#)?;
424        }
425        write!(fmt, ">")
426    }
427}
428
429pub struct Text {
430    pub name: Cow<'static, str>,
431    pub value: Option<Cow<'static, str>>,
432}
433
434impl fmt::Display for Text {
435    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
436        write!(fmt, r#"<input type="text" name="{}""#, self.name)?;
437        if let Some(s) = &self.value {
438            write!(fmt, r#" value="{s}""#)?;
439        }
440        write!(fmt, ">")
441    }
442}
443
444pub trait ToField {
445    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field;
446}
447
448impl ToField for bool {
449    fn to_field(name: Cow<'static, str>, _: &[(&str, &str)]) -> Field {
450        Field::Checkbox(Checkbox {
451            name,
452            checked: false,
453        })
454    }
455}
456
457impl ToField for String {
458    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
459        for (key, value) in params {
460            if *key == "type" {
461                if *value == "hidden" {
462                    return Field::Hidden(Hidden::from_params(name, params));
463                } else if *value == "email" {
464                    return Field::Email(Email { name, value: None });
465                } else if *value == "password" {
466                    return Field::Password(Password { name, value: None });
467                }
468            }
469        }
470        Field::Text(Text { name, value: None })
471    }
472}
473
474impl ToField for Cow<'_, str> {
475    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
476        for (key, value) in params {
477            if *key == "type" {
478                if *value == "hidden" {
479                    return Field::Hidden(Hidden::from_params(name, params));
480                } else if *value == "email" {
481                    return Field::Email(Email { name, value: None });
482                } else if *value == "password" {
483                    return Field::Password(Password { name, value: None });
484                }
485            }
486        }
487        Field::Text(Text { name, value: None })
488    }
489}
490
491impl ToField for u8 {
492    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
493        for (key, value) in params {
494            if *key == "type" && *value == "hidden" {
495                return Field::Hidden(Hidden::from_params(name, params));
496            }
497        }
498        Field::Number(Number { name, value: None })
499    }
500}
501
502impl ToField for u16 {
503    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
504        for (key, value) in params {
505            if *key == "type" && *value == "hidden" {
506                return Field::Hidden(Hidden::from_params(name, params));
507            }
508        }
509        Field::Number(Number { name, value: None })
510    }
511}
512
513impl ToField for u32 {
514    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
515        for (key, value) in params {
516            if *key == "type" && *value == "hidden" {
517                return Field::Hidden(Hidden::from_params(name, params));
518            }
519        }
520        Field::Number(Number { name, value: None })
521    }
522}
523
524impl ToField for u64 {
525    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
526        for (key, value) in params {
527            if *key == "type" && *value == "hidden" {
528                return Field::Hidden(Hidden::from_params(name, params));
529            }
530        }
531        Field::Number(Number { name, value: None })
532    }
533}
534
535impl ToField for i32 {
536    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
537        for (key, value) in params {
538            if *key == "type" && *value == "hidden" {
539                return Field::Hidden(Hidden::from_params(name, params));
540            }
541        }
542        Field::Number(Number { name, value: None })
543    }
544}
545
546impl ToField for i64 {
547    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
548        for (key, value) in params {
549            if *key == "type" && *value == "hidden" {
550                return Field::Hidden(Hidden::from_params(name, params));
551            }
552        }
553        Field::Number(Number { name, value: None })
554    }
555}
556
557impl ToField for f32 {
558    fn to_field(name: Cow<'static, str>, params: &[(&str, &str)]) -> Field {
559        for (key, value) in params {
560            if *key == "type" && *value == "hidden" {
561                return Field::Hidden(Hidden::from_params(name, params));
562            }
563        }
564        Field::Number(Number { name, value: None })
565    }
566}
567
568#[cfg(feature = "chrono")]
569#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
570impl ToField for chrono::NaiveDate {
571    fn to_field(name: Cow<'static, str>, _: &[(&str, &str)]) -> Field {
572        Field::Date(Date { name, value: None })
573    }
574}
575
576#[derive(Debug, Error)]
577pub enum Error {
578    #[error("invalid value for boolean field")]
579    SetInvalidBooleanValue,
580    #[error("no option with given value found in select")]
581    SetOptionNotFound,
582    #[error("unable to set value for unknown field")]
583    SetUnknownField,
584    #[error("setting value not supported for this field type")]
585    SetUnsupportedFieldType,
586}