Skip to content

Commit d1c6d72

Browse files
authored
feat!: add support for file fields in forms (#334)
1 parent a4e7f0d commit d1c6d72

File tree

20 files changed

+1168
-180
lines changed

20 files changed

+1168
-180
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ members = [
55
"cot-codegen",
66
"cot-macros",
77
# Examples
8-
"examples/hello-world",
9-
"examples/todo-list",
10-
"examples/sessions",
118
"examples/admin",
12-
"examples/json",
13-
"examples/custom-task",
149
"examples/custom-error-pages",
10+
"examples/custom-task",
11+
"examples/file-upload",
12+
"examples/hello-world",
13+
"examples/json",
14+
"examples/sessions",
15+
"examples/todo-list",
1516
]
1617
resolver = "2"
1718

@@ -86,13 +87,14 @@ hex = "0.4"
8687
hmac = "0.12"
8788
http = "1.3"
8889
http-body = "1"
89-
http-body-util = "0.1"
90+
http-body-util = "0.1.3"
9091
humantime = "2"
9192
indexmap = "2"
9293
insta = { version = "1", features = ["filters"] }
9394
insta-cmd = "0.6"
9495
mime_guess = { version = "2", default-features = false }
9596
mockall = "0.13"
97+
multer = "3"
9698
password-auth = { version = "1", default-features = false }
9799
petgraph = { version = "0.8", default-features = false }
98100
pin-project-lite = "0.2"

cot-macros/src/admin.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ impl AdminModelDeriveBuilder {
159159
::std::boxed::Box::new(<<Self as #crate_ident::form::Form>::Context as #crate_ident::form::FormContext>::new())
160160
}
161161

162-
fn form_context_from_self(&self) -> ::std::boxed::Box<dyn #crate_ident::form::FormContext> {
163-
::std::boxed::Box::new(<Self as #crate_ident::form::Form>::to_context(self))
162+
async fn form_context_from_self(&self) -> ::std::boxed::Box<dyn #crate_ident::form::FormContext> {
163+
::std::boxed::Box::new(<Self as #crate_ident::form::Form>::to_context(self).await)
164164
}
165165

166166
async fn save_from_request(

cot-macros/src/form.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ impl FormDeriveBuilder {
147147

148148
self.fields_as_context_from_request
149149
.push(quote!(stringify!(#field_ident) => {
150-
#crate_ident::form::FormField::set_value(&mut self.#field_ident, value)
150+
#crate_ident::form::FormField::set_value(&mut self.#field_ident, value).await?
151151
}));
152152

153153
let val_ident = format_ident!("val_{}", field_ident);
@@ -160,7 +160,7 @@ impl FormDeriveBuilder {
160160
quote!(#field_ident: #val_ident.expect("Errors should have been returned by now")),
161161
);
162162
self.fields_as_to_context
163-
.push(quote!(context.#field_ident.set_value(::std::borrow::Cow::Owned(self.#field_ident.to_field_value()))));
163+
.push(quote!(context.#field_ident.set_value(#crate_ident::form::FormFieldValue::new_text(self.#field_ident.to_field_value())).await.expect("Setting value from text should never fail")));
164164

165165
self.fields_as_errors
166166
.push(quote!(#field_ident: Vec<#crate_ident::form::FormFieldValidationError>));
@@ -215,7 +215,7 @@ impl FormDeriveBuilder {
215215
}
216216
}
217217

218-
fn to_context(
218+
async fn to_context(
219219
&self
220220
) -> Self::Context {
221221
use #crate_ident::form::FormContext;
@@ -272,6 +272,7 @@ impl FormDeriveBuilder {
272272
#( #fields_as_struct_fields, )*
273273
}
274274

275+
#[#crate_ident::__private::async_trait]
275276
#[automatically_derived]
276277
impl #crate_ident::form::FormContext for #context_struct_name {
277278
fn new() -> Self {
@@ -289,10 +290,10 @@ impl FormDeriveBuilder {
289290
Box::new([#( #fields_as_dyn_field_ref, )*].into_iter())
290291
}
291292

292-
fn set_value(
293+
async fn set_value(
293294
&mut self,
294295
field_id: &str,
295-
value: ::std::borrow::Cow<str>,
296+
value: #crate_ident::form::FormFieldValue<'_>,
296297
) -> ::core::result::Result<(), #crate_ident::form::FormFieldValidationError> {
297298
match field_id {
298299
#( #fields_as_context_from_request, )*

cot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ http.workspace = true
4141
humantime.workspace = true
4242
indexmap.workspace = true
4343
mime_guess.workspace = true
44+
multer.workspace = true
4445
password-auth = { workspace = true, features = ["std", "argon2"] }
4546
pin-project-lite.workspace = true
4647
schemars = { workspace = true, optional = true }

cot/src/admin.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ async fn edit_model_instance_impl(
304304
} else if let Some(object_id) = object_id {
305305
let object = get_object(&mut request, &*manager, object_id).await?;
306306

307-
manager.form_context_from_object(object)
307+
manager.form_context_from_object(object).await
308308
} else {
309309
manager.form_context()
310310
};
@@ -441,7 +441,7 @@ pub trait AdminModelManager: Send + Sync {
441441
/// that if you always return the same object type from these methods,
442442
/// you can safely downcast the object to the same type in this method
443443
/// as well.
444-
fn form_context_from_object(&self, object: Box<dyn AdminModel>) -> Box<dyn FormContext>;
444+
async fn form_context_from_object(&self, object: Box<dyn AdminModel>) -> Box<dyn FormContext>;
445445

446446
/// Saves the object by using the form data from given request.
447447
///
@@ -531,13 +531,13 @@ impl<T: AdminModel + Send + Sync + 'static> AdminModelManager for DefaultAdminMo
531531
T::form_context()
532532
}
533533

534-
fn form_context_from_object(&self, object: Box<dyn AdminModel>) -> Box<dyn FormContext> {
534+
async fn form_context_from_object(&self, object: Box<dyn AdminModel>) -> Box<dyn FormContext> {
535535
let object_casted = object
536536
.as_any()
537537
.downcast_ref::<T>()
538538
.expect("Invalid object type");
539539

540-
T::form_context_from_self(object_casted)
540+
T::form_context_from_self(object_casted).await
541541
}
542542

543543
async fn save_from_request(
@@ -603,7 +603,7 @@ pub trait AdminModel: Any + Send + 'static {
603603
Self: Sized;
604604

605605
/// Get the form context with the data pre-filled from this model instance.
606-
fn form_context_from_self(&self) -> Box<dyn FormContext>;
606+
async fn form_context_from_self(&self) -> Box<dyn FormContext>;
607607

608608
/// Save the model instance from the form data in the request.
609609
///

cot/src/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ impl_error_from_repr!(crate::router::path::ReverseError);
149149
impl_error_from_repr!(crate::db::DatabaseError);
150150
impl_error_from_repr!(tower_sessions::session::Error);
151151
impl_error_from_repr!(crate::form::FormError);
152+
impl_error_from_repr!(crate::form::FormFieldValueError);
152153
impl_error_from_repr!(crate::auth::AuthError);
153154
impl_error_from_repr!(crate::request::PathParamsDeserializerError);
154155
impl_error_from_repr!(crate::request::extractors::StaticFilesGetError);
@@ -189,6 +190,12 @@ pub(crate) enum ErrorRepr {
189190
expected: &'static str,
190191
actual: String,
191192
},
193+
/// The request does not contain a form.
194+
#[error(
195+
"Request does not contain a form (expected `application/x-www-form-urlencoded` or \
196+
`multipart/form-data` content type, or a GET or HEAD request)"
197+
)]
198+
ExpectedForm,
192199
/// Could not find a route for the request.
193200
#[error("Not found: {message:?}")]
194201
NotFound { message: Option<String> },
@@ -218,6 +225,9 @@ pub(crate) enum ErrorRepr {
218225
/// An error occurred while parsing a form.
219226
#[error("Failed to process a form: {0}")]
220227
Form(#[from] crate::form::FormError),
228+
/// An error occurred while trying to retrieve the value of a form field.
229+
#[error("Failed to retrieve the value of a form field: {0}")]
230+
FormFieldValueError(#[from] crate::form::FormFieldValueError),
221231
/// An error occurred while trying to authenticate a user.
222232
#[error("Failed to authenticate user: {0}")]
223233
Authentication(#[from] crate::auth::AuthError),

0 commit comments

Comments
 (0)