Skip to content

Commit 0ef6325

Browse files
authored
feat: add derive macro SelectAsFormField (#397)
1 parent 6dc14ac commit 0ef6325

10 files changed

Lines changed: 470 additions & 42 deletions

cot-macros/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod from_request;
66
mod main_fn;
77
mod model;
88
mod query;
9+
mod select_as_form_field;
910
mod select_choice;
1011

1112
use darling::Error;
@@ -23,6 +24,7 @@ use crate::from_request::impl_from_request_head_for_struct;
2324
use crate::main_fn::{fn_to_cot_e2e_test, fn_to_cot_main, fn_to_cot_test};
2425
use crate::model::impl_model_for_struct;
2526
use crate::query::{Query, query_to_tokens};
27+
use crate::select_as_form_field::impl_select_as_form_field_for_enum;
2628
use crate::select_choice::impl_select_choice_for_enum;
2729

2830
#[proc_macro_derive(Form, attributes(form))]
@@ -213,6 +215,13 @@ pub fn derive_select_choice(input: TokenStream) -> TokenStream {
213215
token_stream.into()
214216
}
215217

218+
#[proc_macro_derive(SelectAsFormField)]
219+
pub fn derive_select_as_form_field(input: TokenStream) -> TokenStream {
220+
let ast = syn::parse_macro_input!(input as DeriveInput);
221+
let token_stream = impl_select_as_form_field_for_enum(&ast);
222+
token_stream.into()
223+
}
224+
216225
#[proc_macro_derive(IntoResponse)]
217226
pub fn derive_into_response(input: TokenStream) -> TokenStream {
218227
let ast = parse_macro_input!(input as DeriveInput);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use darling::Error;
2+
use quote::quote;
3+
use syn::{Data, DeriveInput};
4+
5+
use crate::cot_ident;
6+
7+
pub(super) fn impl_select_as_form_field_for_enum(ast: &DeriveInput) -> proc_macro2::TokenStream {
8+
let enum_name = &ast.ident;
9+
let cot = cot_ident();
10+
11+
match &ast.data {
12+
Data::Enum(_) => {}
13+
_ => {
14+
return Error::custom("`SelectAsFormField` can only be derived for enums")
15+
.write_errors();
16+
}
17+
}
18+
19+
let impl_single = quote! {
20+
#[automatically_derived]
21+
impl #cot::form::AsFormField for #enum_name {
22+
type Type = #cot::form::fields::SelectField<Self>;
23+
24+
fn clean_value(
25+
field: &Self::Type
26+
) -> ::core::result::Result<Self, #cot::form::FormFieldValidationError> {
27+
match #cot::form::FormField::value(field) {
28+
::core::option::Option::Some(v) if !v.is_empty() => <Self as #cot::form::fields::SelectChoice>::from_str(v),
29+
_ => ::core::result::Result::Err(#cot::form::FormFieldValidationError::Required),
30+
}
31+
}
32+
33+
fn to_field_value(&self) -> ::std::string::String {
34+
<Self as #cot::form::fields::SelectChoice>::to_string(self)
35+
}
36+
}
37+
};
38+
39+
quote! {
40+
#impl_single
41+
}
42+
}

cot-macros/tests/compile_tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ fn derive_select_choice() {
113113
t.compile_fail("tests/ui/derive_select_choice_empty_enum.rs");
114114
}
115115

116+
#[rustversion::attr(
117+
not(nightly),
118+
ignore = "only test on nightly for consistent error messages"
119+
)]
120+
#[test]
121+
#[cfg_attr(
122+
miri,
123+
ignore = "unsupported operation: extern static `pidfd_spawnp` is not supported by Miri"
124+
)]
125+
fn derive_select_as_form_field() {
126+
let t = trybuild::TestCases::new();
127+
t.pass("tests/ui/derive_select_as_form_field.rs");
128+
t.compile_fail("tests/ui/derive_select_as_form_field_struct.rs");
129+
}
130+
116131
#[rustversion::attr(
117132
not(nightly),
118133
ignore = "only test on nightly for consistent error messages"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use cot::form::fields::{SelectChoice, SelectAsFormField};
2+
3+
#[derive(SelectChoice, SelectAsFormField, Debug, Clone, PartialEq, Eq, Hash)]
4+
enum Status {
5+
Draft,
6+
Published,
7+
Archived,
8+
}
9+
10+
fn main() {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use cot_macros::SelectAsFormField;
2+
3+
#[derive(SelectAsFormField)]
4+
struct NotAnEnum {
5+
x: u8,
6+
}
7+
8+
fn main() {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
error: `SelectAsFormField` can only be derived for enums
2+
--> tests/ui/derive_select_as_form_field_struct.rs:3:10
3+
|
4+
3 | #[derive(SelectAsFormField)]
5+
| ^^^^^^^^^^^^^^^^^
6+
|
7+
= note: this error originates in the derive macro `SelectAsFormField` (in Nightly builds, run with -Z macro-backtrace for more info)

cot/src/form/fields.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ pub use chrono::{
1818
pub use files::{FileField, FileFieldOptions, InMemoryUploadedFile};
1919
pub(crate) use select::check_required_multiple;
2020
pub use select::{
21-
SelectChoice, SelectField, SelectFieldOptions, SelectMultipleField, SelectMultipleFieldOptions,
21+
SelectAsFormField, SelectChoice, SelectField, SelectFieldOptions, SelectMultipleField,
22+
SelectMultipleFieldOptions,
2223
};
2324

2425
use crate::auth::PasswordHash;

cot/src/form/fields/chrono.rs

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ use cot::form::FormField;
1010
use cot::form::fields::impl_form_field;
1111
use cot::html::HtmlTag;
1212

13-
use crate::form::fields::{
14-
SelectChoice, SelectField, SelectMultipleField, Step, check_required, check_required_multiple,
15-
};
13+
use crate::form::fields::{SelectChoice, SelectField, Step, check_required};
1614
use crate::form::{AsFormField, FormFieldValidationError};
1715

1816
impl AsFormField for Weekday {
@@ -29,39 +27,7 @@ impl AsFormField for Weekday {
2927
}
3028
}
3129

32-
macro_rules! impl_as_form_field_mult {
33-
($field_type:ty) => {
34-
impl_as_form_field_mult_collection!(::std::vec::Vec<$field_type>, $field_type);
35-
impl_as_form_field_mult_collection!(::std::collections::VecDeque<$field_type>, $field_type);
36-
impl_as_form_field_mult_collection!(
37-
::std::collections::LinkedList<$field_type>,
38-
$field_type
39-
);
40-
impl_as_form_field_mult_collection!(::std::collections::HashSet<$field_type>, $field_type);
41-
impl_as_form_field_mult_collection!(::indexmap::IndexSet<$field_type>, $field_type);
42-
};
43-
}
44-
45-
macro_rules! impl_as_form_field_mult_collection {
46-
($collection_type:ty, $field_type:ty) => {
47-
impl AsFormField for $collection_type {
48-
type Type = SelectMultipleField<$field_type>;
49-
50-
fn clean_value(field: &Self::Type) -> Result<Self, FormFieldValidationError> {
51-
let value = check_required_multiple(field)?;
52-
53-
value.iter().map(|id| <$field_type>::from_str(id)).collect()
54-
}
55-
56-
fn to_field_value(&self) -> String {
57-
String::new()
58-
}
59-
}
60-
};
61-
}
62-
63-
impl_as_form_field_mult!(Weekday);
64-
impl_as_form_field_mult_collection!(WeekdaySet, Weekday);
30+
crate::form::fields::select::impl_as_form_field_mult_collection!(() => WeekdaySet, Weekday);
6531

6632
const MONDAY_ID: &str = "mon";
6733
const TUESDAY_ID: &str = "tue";
@@ -730,7 +696,9 @@ mod tests {
730696
use cot::form::FormFieldValue;
731697

732698
use super::*;
733-
use crate::form::fields::{SelectFieldOptions, SelectMultipleFieldOptions};
699+
use crate::form::fields::{
700+
SelectFieldOptions, SelectMultipleField, SelectMultipleFieldOptions,
701+
};
734702
use crate::form::{FormField, FormFieldOptions};
735703

736704
#[test]

0 commit comments

Comments
 (0)