Skip to content

Commit 477728e

Browse files
authored
feat!: add SelectField; support more chrono form fields (#345)
1 parent 1768bc3 commit 477728e

File tree

11 files changed

+1989
-302
lines changed

11 files changed

+1989
-302
lines changed

cot-cli/src/project_template/Cargo.lock.template

Lines changed: 218 additions & 259 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cot-macros/src/form.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use proc_macro2::TokenStream;
66
use quote::{ToTokens, format_ident, quote};
77

88
use crate::cot_ident;
9+
use crate::utils::PreservedStrExpr;
910

1011
pub(super) fn impl_form_for_struct(ast: &syn::DeriveInput) -> TokenStream {
1112
let opts = match FormOpts::from_derive_input(ast) {
@@ -70,7 +71,7 @@ impl FormOpts {
7071
struct Field {
7172
ident: Option<syn::Ident>,
7273
ty: syn::Type,
73-
opt: Option<HashMap<syn::Ident, syn::Expr>>,
74+
opt: Option<HashMap<syn::Ident, PreservedStrExpr>>,
7475
}
7576

7677
#[derive(Debug)]
@@ -126,7 +127,9 @@ impl FormDeriveBuilder {
126127
self.fields_as_struct_fields_new.push({
127128
let custom_options_setters: Vec<_> = if let Some(opt) = opt {
128129
opt.iter()
129-
.map(|(key, value)| quote!(custom_options.#key = Some(#value)))
130+
.map(|(key, value)| {
131+
quote!(custom_options.#key = ::core::option::Option::Some(#value))
132+
})
130133
.collect()
131134
} else {
132135
Vec::new()

cot-macros/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod form;
44
mod main_fn;
55
mod model;
66
mod query;
7+
mod utils;
78

89
use darling::Error;
910
use darling::ast::NestedMeta;

cot-macros/src/utils.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// TODO: This should be removed when https://github.com/TedDriggs/darling/pull/346
2+
// is merged and released
3+
4+
use darling::{FromMeta, Result};
5+
use syn::Expr;
6+
7+
/// A wrapper around [`Expr`] that preserves the original expression
8+
/// without evaluating it.
9+
///
10+
/// For compatibility reasons, `darling` evaluates the expression inside string
11+
/// literals, which might be undesirable. In many cases,
12+
/// [`darling::util::parse_expr::preserve_str_literal`] can be used. However,
13+
/// when using [`Expr`] inside a container (such as a
14+
/// [`HashMap`](std::collections::HashMap)), it is not possible to use it.
15+
///
16+
/// This wrapper preserves the original expression without evaluating it.
17+
///
18+
/// # Example
19+
///
20+
/// ```compile_fail
21+
/// #[derive(FromMeta)]
22+
/// #[darling(attributes(demo))]
23+
/// struct Demo {
24+
/// option: Option<HashMap<syn::Ident, PreservedStrExpr>>,
25+
/// }
26+
/// ```
27+
#[repr(transparent)]
28+
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
29+
pub(crate) struct PreservedStrExpr(pub Expr);
30+
31+
impl FromMeta for PreservedStrExpr {
32+
fn from_expr(expr: &Expr) -> Result<Self> {
33+
Ok(Self(expr.clone()))
34+
}
35+
}
36+
37+
impl From<Expr> for PreservedStrExpr {
38+
fn from(value: Expr) -> Self {
39+
Self(value)
40+
}
41+
}
42+
43+
impl From<PreservedStrExpr> for Expr {
44+
fn from(value: PreservedStrExpr) -> Self {
45+
value.0
46+
}
47+
}
48+
49+
impl quote::ToTokens for PreservedStrExpr {
50+
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
51+
self.0.to_tokens(tokens);
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use syn::{Meta, MetaNameValue, parse_quote};
58+
59+
use super::*;
60+
61+
#[test]
62+
fn preserved_str_expr_from_meta() {
63+
let name_value: MetaNameValue = parse_quote!(test = "Hello, world!");
64+
let preserved = PreservedStrExpr::from_meta(&Meta::NameValue(name_value)).unwrap();
65+
66+
assert_eq!(preserved.0, parse_quote!("Hello, world!"));
67+
}
68+
}

cot/src/form.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,12 @@ pub trait FormField: Display {
521521
///
522522
/// This method should convert the value to the appropriate type for the
523523
/// field, such as a number for a number field.
524+
///
525+
/// Note that this method might be called multiple times. This will happen
526+
/// when the field has appeared in the form data multiple times, such as
527+
/// in the case of a `<select multiple>` HTML element. If the field
528+
/// does not support storing multiple values, it should overwrite the
529+
/// previous value.
524530
fn set_value(
525531
&mut self,
526532
field: FormFieldValue<'_>,

cot/src/form/fields.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
mod chrono;
12
mod files;
3+
mod select;
24

35
use std::fmt::{Debug, Display, Formatter};
46
use std::num::{
@@ -8,32 +10,33 @@ use std::num::{
810

911
use askama::filters::HtmlSafe;
1012
pub use files::{FileField, FileFieldOptions, InMemoryUploadedFile};
13+
pub(crate) use select::check_required_multiple;
14+
pub use select::{
15+
SelectChoice, SelectField, SelectFieldOptions, SelectMultipleField, SelectMultipleFieldOptions,
16+
};
1117

1218
use crate::auth::PasswordHash;
1319
use crate::common_types::{Email, Password};
1420
#[cfg(feature = "db")]
1521
use crate::db::{Auto, ForeignKey, LimitedString, Model};
16-
use crate::form::{
17-
AsFormField, FormField, FormFieldOptions, FormFieldValidationError, FormFieldValue,
18-
FormFieldValueError,
19-
};
22+
use crate::form::{AsFormField, FormField, FormFieldOptions, FormFieldValidationError};
2023
use crate::html::HtmlTag;
2124

2225
macro_rules! impl_form_field {
2326
($field_type_name:ident, $field_options_type_name:ident, $purpose:literal $(, $generic_param:ident $(: $generic_param_bound:ident $(+ $generic_param_bound_more:ident)*)?)?) => {
2427
#[derive(Debug)]
2528
#[doc = concat!("A form field for ", $purpose, ".")]
2629
pub struct $field_type_name $(<$generic_param>)? {
27-
options: FormFieldOptions,
30+
options: $crate::form::FormFieldOptions,
2831
custom_options: $field_options_type_name $(<$generic_param>)?,
2932
value: Option<String>,
3033
}
3134

32-
impl $(<$generic_param $(: $generic_param_bound $(+ $generic_param_bound_more)* )?>)? FormField for $field_type_name $(<$generic_param>)? {
35+
impl $(<$generic_param $(: $generic_param_bound $(+ $generic_param_bound_more)* )?>)? $crate::form::FormField for $field_type_name $(<$generic_param>)? {
3336
type CustomOptions = $field_options_type_name $(<$generic_param>)?;
3437

3538
fn with_options(
36-
options: FormFieldOptions,
39+
options: $crate::form::FormFieldOptions,
3740
custom_options: Self::CustomOptions,
3841
) -> Self {
3942
Self {
@@ -43,21 +46,22 @@ macro_rules! impl_form_field {
4346
}
4447
}
4548

46-
fn options(&self) -> &FormFieldOptions {
49+
fn options(&self) -> &$crate::form::FormFieldOptions {
4750
&self.options
4851
}
4952

5053
fn value(&self) -> Option<&str> {
5154
self.value.as_deref()
5255
}
5356

54-
async fn set_value(&mut self, field: FormFieldValue<'_>) -> std::result::Result<(), FormFieldValueError> {
57+
async fn set_value(&mut self, field: $crate::form::FormFieldValue<'_>) -> std::result::Result<(), $crate::form::FormFieldValueError> {
5558
self.value = Some(field.into_text().await?);
5659
Ok(())
5760
}
5861
}
5962
};
6063
}
64+
pub(crate) use impl_form_field;
6165

6266
impl_form_field!(StringField, StringFieldOptions, "a string");
6367

@@ -220,7 +224,7 @@ pub struct EmailFieldOptions {
220224
impl Display for EmailField {
221225
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
222226
let mut tag = HtmlTag::input("email");
223-
tag.attr("name", self.name());
227+
tag.attr("name", self.id());
224228
tag.attr("id", self.id());
225229
if self.options.required {
226230
tag.bool_attr("required");
@@ -624,7 +628,7 @@ where
624628
}
625629
}
626630

627-
fn check_required<T: FormField>(field: &T) -> Result<&str, FormFieldValidationError> {
631+
pub(crate) fn check_required<T: FormField>(field: &T) -> Result<&str, FormFieldValidationError> {
628632
if let Some(value) = field.value() {
629633
if value.is_empty() {
630634
Err(FormFieldValidationError::Required)
@@ -661,7 +665,7 @@ impl<T: Float> Default for FloatFieldOptions<T> {
661665
impl<T: Float> Display for FloatField<T> {
662666
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
663667
let mut tag: HtmlTag = HtmlTag::input("number");
664-
tag.attr("name", self.name());
668+
tag.attr("name", self.id());
665669
tag.attr("id", self.id());
666670
if self.options.required {
667671
tag.bool_attr("required");
@@ -766,6 +770,7 @@ impl_float_as_form_field!(f64);
766770
#[cfg(test)]
767771
mod tests {
768772
use super::*;
773+
use crate::form::FormFieldValue;
769774

770775
#[test]
771776
fn string_field_render() {
@@ -822,7 +827,7 @@ mod tests {
822827
assert!(html.contains("required"));
823828
assert!(html.contains("minlength=\"10\""));
824829
assert!(html.contains("maxlength=\"50\""));
825-
assert!(html.contains("name=\"test_name\""));
830+
assert!(html.contains("name=\"test_id\""));
826831
assert!(html.contains("id=\"test_id\""));
827832
}
828833

0 commit comments

Comments
 (0)