From b3da91cf8de4ccfd9f4c2d9c060f0c098371dd16 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 16:19:28 +0100 Subject: [PATCH 01/14] create new crate --- Cargo.lock | 4 ++++ Cargo.toml | 2 ++ cot-core/Cargo.toml | 18 ++++++++++++++++++ cot-core/src/lib.rs | 14 ++++++++++++++ cot/Cargo.toml | 1 + 5 files changed, 39 insertions(+) create mode 100644 cot-core/Cargo.toml create mode 100644 cot-core/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8001be5e6..12bed6f60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -777,6 +777,10 @@ dependencies = [ "trybuild", ] +[[package]] +name = "cot-core" +version = "0.4.0" + [[package]] name = "cot_codegen" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 8d793847b..77bdaf7f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "cot-cli", "cot-codegen", "cot-macros", + "cot-core", # Examples "examples/admin", "examples/custom-error-pages", @@ -73,6 +74,7 @@ clap-verbosity-flag = { version = "3", default-features = false } clap_complete = "4" clap_mangen = "0.2.31" cot = { version = "0.4.0", path = "cot" } +cot_core = { version = "0.4.0", path = "cot-core" } cot_codegen = { version = "0.4.0", path = "cot-codegen" } cot_macros = { version = "0.4.0", path = "cot-macros" } criterion = "0.8" diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml new file mode 100644 index 000000000..171562963 --- /dev/null +++ b/cot-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cot-core" +version = "0.4.0" +description = "The Rust web framework for lazy developers - framework core." +categories = ["web-programming", "web-programming::http-server", "network-programming"] +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +readme.workspace = true +authors.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs new file mode 100644 index 000000000..b93cf3ffd --- /dev/null +++ b/cot-core/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 1cb081b63..2823cbb1f 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -25,6 +25,7 @@ bytes.workspace = true chrono = { workspace = true, features = ["alloc", "serde", "clock"] } chrono-tz.workspace = true clap.workspace = true +cot_core.workspace = true cot_macros.workspace = true deadpool-redis = { workspace = true, features = ["tokio-comp", "rt_tokio_1"], optional = true } derive_builder.workspace = true From 665082c091fb4e02e32f3140a83a8dbe9e9e0818 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 17:12:18 +0100 Subject: [PATCH 02/14] add basic types --- Cargo.lock | 12 ++++++++---- cot-core/Cargo.toml | 3 ++- cot-core/src/lib.rs | 17 ++++------------- cot/src/lib.rs | 7 ++----- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12bed6f60..8f560378e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,6 +686,7 @@ dependencies = [ "chrono", "chrono-tz", "clap", + "cot_core", "cot_macros", "criterion", "deadpool-redis", @@ -777,10 +778,6 @@ dependencies = [ "trybuild", ] -[[package]] -name = "cot-core" -version = "0.4.0" - [[package]] name = "cot_codegen" version = "0.4.0" @@ -793,6 +790,13 @@ dependencies = [ "tracing", ] +[[package]] +name = "cot_core" +version = "0.4.0" +dependencies = [ + "http 1.4.0", +] + [[package]] name = "cot_macros" version = "0.4.0" diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 171562963..ac712b5ee 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cot-core" +name = "cot_core" version = "0.4.0" description = "The Rust web framework for lazy developers - framework core." categories = ["web-programming", "web-programming::http-server", "network-programming"] @@ -16,3 +16,4 @@ authors.workspace = true workspace = true [dependencies] +http.workspace = true diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index b93cf3ffd..5494e903c 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,14 +1,5 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +/// A type alias for an HTTP status code. +pub type StatusCode = http::StatusCode; -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +/// A type alias for an HTTP method. +pub type Method = http::Method; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 3cfb59940..54e9e42e3 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -172,8 +172,5 @@ pub use crate::project::{ /// A type alias for a result that can return a [`cot::Error`]. pub type Result = std::result::Result; -/// A type alias for an HTTP status code. -pub type StatusCode = http::StatusCode; - -/// A type alias for an HTTP method. -pub type Method = http::Method; +#[doc(inline)] +pub use cot_core::{Method, StatusCode}; From 383b9f5435e969b6b0fa42ec8059a26160a7d51a Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 17:43:13 +0100 Subject: [PATCH 03/14] move error --- Cargo.lock | 11 +- cot-core/Cargo.toml | 9 + cot-core/src/error.rs | 10 + {cot => cot-core}/src/error/backtrace.rs | 12 +- {cot => cot-core}/src/error/error_impl.rs | 5 +- cot-core/src/error/handler.rs | 302 ++++++++++++++++++ .../src/error/method_not_allowed.rs | 0 {cot => cot-core}/src/error/not_found.rs | 2 +- {cot => cot-core}/src/error/uncaught_panic.rs | 0 cot-core/src/lib.rs | 3 + cot/src/auth.rs | 2 +- cot/src/body.rs | 2 +- cot/src/cache.rs | 2 +- cot/src/cache/store.rs | 2 +- cot/src/cache/store/redis.rs | 2 +- cot/src/config.rs | 2 +- cot/src/db.rs | 2 +- cot/src/error.rs | 12 +- cot/src/error_page.rs | 7 +- cot/src/form.rs | 2 +- cot/src/form/field_value.rs | 2 +- cot/src/lib.rs | 3 +- cot/src/project.rs | 3 +- cot/src/request.rs | 2 +- cot/src/request/extractors.rs | 2 +- cot/src/response/into_response.rs | 2 +- cot/src/router.rs | 3 +- cot/src/router/path.rs | 2 +- cot/src/static_files.rs | 2 +- cot/src/test.rs | 2 +- 30 files changed, 370 insertions(+), 42 deletions(-) create mode 100644 cot-core/src/error.rs rename {cot => cot-core}/src/error/backtrace.rs (94%) rename {cot => cot-core}/src/error/error_impl.rs (99%) create mode 100644 cot-core/src/error/handler.rs rename {cot => cot-core}/src/error/method_not_allowed.rs (100%) rename {cot => cot-core}/src/error/not_found.rs (98%) rename {cot => cot-core}/src/error/uncaught_panic.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 8f560378e..b64eb8aea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -794,7 +794,14 @@ dependencies = [ name = "cot_core" version = "0.4.0" dependencies = [ + "askama", + "backtrace", + "derive_more", "http 1.4.0", + "serde", + "serde_json", + "thiserror 2.0.17", + "tower-sessions", ] [[package]] @@ -3210,9 +3217,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index ac712b5ee..6f8d083ef 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -17,3 +17,12 @@ workspace = true [dependencies] http.workspace = true +derive_more.workspace = true +thiserror.workspace = true +serde.workspace = true +serde_json.workspace = true +backtrace.workspace = true + +# Trait implementations +askama.workspace = true +tower-sessions.workspace = true diff --git a/cot-core/src/error.rs b/cot-core/src/error.rs new file mode 100644 index 000000000..592b87ea9 --- /dev/null +++ b/cot-core/src/error.rs @@ -0,0 +1,10 @@ +pub mod backtrace; +pub(crate) mod error_impl; +mod method_not_allowed; +mod not_found; +mod uncaught_panic; + +pub use error_impl::{Error, impl_into_cot_error}; +pub use method_not_allowed::MethodNotAllowed; +pub use not_found::{Kind as NotFoundKind, NotFound}; +pub use uncaught_panic::UncaughtPanic; diff --git a/cot/src/error/backtrace.rs b/cot-core/src/error/backtrace.rs similarity index 94% rename from cot/src/error/backtrace.rs rename to cot-core/src/error/backtrace.rs index 9bc27cfe5..9827b76e9 100644 --- a/cot/src/error/backtrace.rs +++ b/cot-core/src/error/backtrace.rs @@ -1,7 +1,7 @@ // inline(never) is added to make sure there is a separate frame for this // function so that it can be used to find the start of the backtrace. #[inline(never)] -pub(crate) fn __cot_create_backtrace() -> Backtrace { +pub fn __cot_create_backtrace() -> Backtrace { let mut backtrace = Vec::new(); let mut start = false; backtrace::trace(|frame| { @@ -21,19 +21,19 @@ pub(crate) fn __cot_create_backtrace() -> Backtrace { } #[derive(Debug, Clone)] -pub(crate) struct Backtrace { +pub struct Backtrace { frames: Vec, } impl Backtrace { #[must_use] - pub(crate) fn frames(&self) -> &[StackFrame] { + pub fn frames(&self) -> &[StackFrame] { &self.frames } } #[derive(Debug, Clone)] -pub(crate) struct StackFrame { +pub struct StackFrame { symbol_name: Option, filename: Option, lineno: Option, @@ -42,7 +42,7 @@ pub(crate) struct StackFrame { impl StackFrame { #[must_use] - pub(crate) fn symbol_name(&self) -> String { + pub fn symbol_name(&self) -> String { self.symbol_name .as_deref() .unwrap_or("") @@ -50,7 +50,7 @@ impl StackFrame { } #[must_use] - pub(crate) fn location(&self) -> String { + pub fn location(&self) -> String { if let Some(filename) = self.filename.as_deref() { let mut s = filename.to_owned(); diff --git a/cot/src/error/error_impl.rs b/cot-core/src/error/error_impl.rs similarity index 99% rename from cot/src/error/error_impl.rs rename to cot-core/src/error/error_impl.rs index dbe2856bf..45f185801 100644 --- a/cot/src/error/error_impl.rs +++ b/cot-core/src/error/error_impl.rs @@ -216,7 +216,7 @@ impl Error { } #[must_use] - pub(crate) fn backtrace(&self) -> &CotBacktrace { + pub fn backtrace(&self) -> &CotBacktrace { &self.repr.backtrace } @@ -319,6 +319,7 @@ impl From for askama::Error { } } +#[macro_export] macro_rules! impl_into_cot_error { ($error_ty:ty) => { impl From<$error_ty> for $crate::Error { @@ -335,7 +336,7 @@ macro_rules! impl_into_cot_error { } }; } -pub(crate) use impl_into_cot_error; +pub use impl_into_cot_error; #[derive(Debug, thiserror::Error)] #[error("failed to render template: {0}")] diff --git a/cot-core/src/error/handler.rs b/cot-core/src/error/handler.rs new file mode 100644 index 000000000..8dd03002e --- /dev/null +++ b/cot-core/src/error/handler.rs @@ -0,0 +1,302 @@ +//! Error handling functionality for custom error pages and handlers. + +use std::fmt::Display; +use std::marker::PhantomData; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use derive_more::with_trait::Debug; + +use crate::Error; +use crate::handler::handle_all_parameters; +use crate::request::extractors::FromRequestHead; +use crate::request::{Request, RequestHead}; +use crate::response::Response; + +/// A trait for handling error pages in Cot applications. +/// +/// This trait is implemented by functions that can handle error pages. The +/// trait is automatically implemented for async functions that take parameters +/// implementing [`FromRequestHead`] and return a type that implements +/// [`IntoResponse`]. +/// +/// # Examples +/// +/// ``` +/// use cot::Project; +/// use cot::error::handler::{DynErrorPageHandler, RequestError}; +/// use cot::html::Html; +/// use cot::response::IntoResponse; +/// +/// struct MyProject; +/// impl Project for MyProject { +/// fn error_handler(&self) -> DynErrorPageHandler { +/// DynErrorPageHandler::new(error_handler) +/// } +/// } +/// +/// // This function automatically implements ErrorPageHandler +/// async fn error_handler(error: RequestError) -> impl IntoResponse { +/// Html::new(format!("An error occurred: {error}")).with_status(error.status_code()) +/// } +/// ``` +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a valid error page handler", + label = "not a valid error page handler", + note = "make sure the function is marked `async`", + note = "make sure all parameters implement `FromRequestHead`", + note = "make sure the function takes no more than 10 parameters", + note = "make sure the function returns a type that implements `IntoResponse`" +)] +pub trait ErrorPageHandler { + /// Handles an error request and returns a response. + /// + /// This method is called when an error occurs and the application needs to + /// generate an error page response. + /// + /// # Errors + /// + /// This method may return an error if the handler fails to build a + /// response. In this case, the error will be logged and a generic + /// error page will be returned to the user. + fn handle(&self, head: &RequestHead) -> impl Future> + Send; +} + +pub(crate) trait BoxErrorPageHandler: Send + Sync { + fn handle<'a>( + &'a self, + head: &'a RequestHead, + ) -> Pin> + Send + 'a>>; +} + +/// A type-erased wrapper around an error page handler. +/// +/// This struct allows storing different types of error page handlers in a +/// homogeneous collection or service. It implements [`Clone`] and can be +/// used with Cot's error handling infrastructure. +#[derive(Debug, Clone)] +pub struct DynErrorPageHandler { + #[debug("..")] + handler: Arc, +} + +impl DynErrorPageHandler { + /// Creates a new `DynErrorPageHandler` from a concrete error page handler. + /// + /// This method wraps a concrete error page handler in a type-erased + /// wrapper, allowing it to be used in + /// [`crate::project::Project::error_handler`]. + /// + /// # Examples + /// + /// ``` + /// use cot::Project; + /// use cot::error::handler::{DynErrorPageHandler, RequestError}; + /// use cot::html::Html; + /// use cot::response::IntoResponse; + /// + /// struct MyProject; + /// impl Project for MyProject { + /// fn error_handler(&self) -> DynErrorPageHandler { + /// DynErrorPageHandler::new(error_handler) + /// } + /// } + /// + /// // This function automatically implements ErrorPageHandler + /// async fn error_handler(error: RequestError) -> impl IntoResponse { + /// Html::new(format!("An error occurred: {error}")).with_status(error.status_code()) + /// } + /// ``` + pub fn new(handler: H) -> Self + where + HandlerParams: 'static, + H: ErrorPageHandler + Send + Sync + 'static, + { + struct Inner(H, PhantomData T>); + + impl + Send + Sync> BoxErrorPageHandler for Inner { + fn handle<'a>( + &'a self, + head: &'a RequestHead, + ) -> Pin> + Send + 'a>> { + Box::pin(self.0.handle(head)) + } + } + + Self { + handler: Arc::new(Inner(handler, PhantomData)), + } + } +} + +impl tower::Service for DynErrorPageHandler { + type Response = Response; + type Error = Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + let handler = self.handler.clone(); + let (head, _) = req.into_parts(); + Box::pin(async move { handler.handle(&head).await }) + } +} + +macro_rules! impl_request_handler { + ($($ty:ident),*) => { + impl ErrorPageHandler<($($ty,)*)> for Func + where + Func: FnOnce($($ty,)*) -> Fut + Clone + Send + Sync + 'static, + $($ty: FromRequestHead + Send,)* + Fut: Future + Send, + R: crate::response::IntoResponse, + { + #[allow( + clippy::allow_attributes, + non_snake_case, + unused_variables, + reason = "for the case where there are no params" + )] + async fn handle(&self, head: &RequestHead) -> crate::Result { + $( + let $ty = <$ty as FromRequestHead>::from_request_head(&head).await?; + )* + + self.clone()($($ty,)*).await.into_response() + } + } + }; +} + +handle_all_parameters!(impl_request_handler); + +/// A wrapper around [`Error`] that contains an error (outermost, +/// possibly middleware-wrapped) to be processed by the error handler. +/// +/// This returns the outermost error returned by the request handler and +/// middlewares. In most cases, you should use [`RequestError`] instead, which +/// dereferences to the inner [`Error`] (the first error in the error chain that +/// contains an explicitly set status code). [`RequestError`] will allow you to +/// check for specific error types even when middleware might have wrapped the +/// error. +#[derive(Debug, Clone)] +pub struct RequestOuterError(Arc); + +impl RequestOuterError { + #[must_use] + pub(crate) fn new(error: Error) -> Self { + Self(Arc::new(error)) + } +} + +impl Deref for RequestOuterError { + type Target = Error; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for RequestOuterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0.inner(), f) + } +} + +impl FromRequestHead for RequestOuterError { + async fn from_request_head(head: &RequestHead) -> crate::Result { + let error = head.extensions.get::(); + error + .ok_or_else(|| { + Error::internal("No error found in request head. Make sure you use this extractor in an error handler.") + }).cloned() + } +} + +/// A wrapper around [`Error`] that contains an error to be processed by the +/// error handler. +/// +/// Note that the [`Deref`] implementation returns the inner [`Error`] (see +/// [`Error::inner`]), which is the first error in the error chain that contains +/// an explicitly set status code. This is usually what you want since it allows +/// you to check for specific error types even when middleware might have +/// wrapped the error. If you need to access the outermost error instead, +/// you can use [`RequestOuterError`]. +#[derive(Debug, Clone)] +pub struct RequestError(Arc); + +impl Deref for RequestError { + type Target = Error; + + fn deref(&self) -> &Self::Target { + self.0.inner() + } +} + +impl Display for RequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0.inner(), f) + } +} + +impl FromRequestHead for RequestError { + async fn from_request_head(head: &RequestHead) -> crate::Result { + let error = head.extensions.get::(); + error + .ok_or_else(|| { + Error::internal( + "No error found in request head. \ + Make sure you use this extractor in an error handler.", + ) + }) + .map(|request_error| Self(request_error.0.clone())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_outer_error_display() { + let error = Error::internal("Test error"); + let request_error = RequestOuterError::new(error); + + assert_eq!(format!("{request_error}"), "Test error"); + } + + #[test] + fn request_error_display() { + let error = Error::internal("Test error"); + let request_error = RequestError(Arc::new(error)); + + assert_eq!(format!("{request_error}"), "Test error"); + } + + #[cot::test] + async fn request_outer_error_from_request_head() { + let request = Request::default(); + let (mut head, _) = request.into_parts(); + head.extensions + .insert(RequestOuterError::new(Error::internal("Test error"))); + + let extracted_error = RequestOuterError::from_request_head(&head).await.unwrap(); + assert_eq!(format!("{extracted_error}"), "Test error"); + } + + #[cot::test] + async fn request_error_from_request_head() { + let request = Request::default(); + let (mut head, _) = request.into_parts(); + head.extensions + .insert(RequestOuterError::new(Error::internal("Test error"))); + + let extracted_error = RequestError::from_request_head(&head).await.unwrap(); + assert_eq!(format!("{extracted_error}"), "Test error"); + } +} diff --git a/cot/src/error/method_not_allowed.rs b/cot-core/src/error/method_not_allowed.rs similarity index 100% rename from cot/src/error/method_not_allowed.rs rename to cot-core/src/error/method_not_allowed.rs diff --git a/cot/src/error/not_found.rs b/cot-core/src/error/not_found.rs similarity index 98% rename from cot/src/error/not_found.rs rename to cot-core/src/error/not_found.rs index f71144baf..820e8fad7 100644 --- a/cot/src/error/not_found.rs +++ b/cot-core/src/error/not_found.rs @@ -72,7 +72,7 @@ impl NotFound { } #[must_use] - pub(crate) fn router() -> Self { + pub fn router() -> Self { Self::with_kind(Kind::FromRouter) } diff --git a/cot/src/error/uncaught_panic.rs b/cot-core/src/error/uncaught_panic.rs similarity index 100% rename from cot/src/error/uncaught_panic.rs rename to cot-core/src/error/uncaught_panic.rs diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index 5494e903c..becc30880 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,3 +1,6 @@ +pub mod error; +pub use error::Error; + /// A type alias for an HTTP status code. pub type StatusCode = http::StatusCode; diff --git a/cot/src/auth.rs b/cot/src/auth.rs index 8dd86daf4..99b4ef0a8 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -27,7 +27,7 @@ use thiserror::Error; use crate::config::SecretKey; #[cfg(feature = "db")] use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue}; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::request::{Request, RequestExt}; use crate::session::Session; diff --git a/cot/src/body.rs b/cot/src/body.rs index 874307c30..fafedaaf7 100644 --- a/cot/src/body.rs +++ b/cot/src/body.rs @@ -9,7 +9,7 @@ use http_body::{Frame, SizeHint}; use http_body_util::combinators::BoxBody; use sync_wrapper::SyncWrapper; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::{Error, Result}; /// A type that represents an HTTP request or response body. diff --git a/cot/src/cache.rs b/cot/src/cache.rs index c1f282e79..36be1684b 100644 --- a/cot/src/cache.rs +++ b/cot/src/cache.rs @@ -126,7 +126,7 @@ use crate::cache::store::memory::Memory; use crate::cache::store::redis::Redis; use crate::cache::store::{BoxCacheStore, CacheStore}; use crate::config::{CacheConfig, Timeout}; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; /// An error that can occur when interacting with the cache. #[derive(Debug, Error)] diff --git a/cot/src/cache/store.rs b/cot/src/cache/store.rs index ef278cbb8..6019ab16a 100644 --- a/cot/src/cache/store.rs +++ b/cot/src/cache/store.rs @@ -16,7 +16,7 @@ use serde_json::Value; use thiserror::Error; use crate::config::Timeout; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; const CACHE_STORE_ERROR_PREFIX: &str = "cache store error:"; diff --git a/cot/src/cache/store/redis.rs b/cot/src/cache/store/redis.rs index 1043da858..2a1c43a5f 100644 --- a/cot/src/cache/store/redis.rs +++ b/cot/src/cache/store/redis.rs @@ -25,7 +25,7 @@ use thiserror::Error; use crate::cache::store::{CacheStore, CacheStoreError}; use crate::config::CacheUrl; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; const ERROR_PREFIX: &str = "redis cache store error:"; diff --git a/cot/src/config.rs b/cot/src/config.rs index f9d76b837..2bac94b81 100644 --- a/cot/src/config.rs +++ b/cot/src/config.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use subtle::ConstantTimeEq; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::utils::chrono::DateTimeWithOffsetAdapter; /// The configuration for a project. diff --git a/cot/src/db.rs b/cot/src/db.rs index 96d7cde9e..21b7150bb 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -42,7 +42,7 @@ use crate::db::impl_postgres::{DatabasePostgres, PostgresRow, PostgresValueRef}; #[cfg(feature = "sqlite")] use crate::db::impl_sqlite::{DatabaseSqlite, SqliteRow, SqliteValueRef}; use crate::db::migrations::ColumnTypeMapper; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; const ERROR_PREFIX: &str = "database error:"; /// An error that can occur when interacting with the database. diff --git a/cot/src/error.rs b/cot/src/error.rs index 2e24fa372..ee998cbd3 100644 --- a/cot/src/error.rs +++ b/cot/src/error.rs @@ -1,10 +1,6 @@ -pub(crate) mod backtrace; -pub(crate) mod error_impl; pub mod handler; -mod method_not_allowed; -mod not_found; -mod uncaught_panic; -pub use method_not_allowed::MethodNotAllowed; -pub use not_found::{Kind as NotFoundKind, NotFound}; -pub use uncaught_panic::UncaughtPanic; +#[doc(inline)] +pub use cot_core::error::{MethodNotAllowed, NotFound, NotFoundKind, UncaughtPanic}; +#[doc(inline)] +pub(crate) use cot_core::error::{backtrace, impl_into_cot_error}; diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index 71629d2ef..b5437a6a6 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -73,14 +73,15 @@ impl ErrorPageTemplateBuilder { if let Some(not_found) = error.inner().downcast_ref::() { use crate::error::NotFoundKind as Kind; match ¬_found.kind { - Kind::FromRouter => {} - Kind::Custom => { + Kind::FromRouter { .. } => {} + Kind::Custom { .. } => { Self::build_error_data(&mut error_data, error); } - Kind::WithMessage(message) => { + Kind::WithMessage { 0: message, .. } => { Self::build_error_data(&mut error_data, error); error_message = Some(message.clone()); } + _ => {} } } diff --git a/cot/src/form.rs b/cot/src/form.rs index 67d790f92..bbf97ed37 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -61,7 +61,7 @@ pub use field_value::{FormFieldValue, FormFieldValueError}; use http_body_util::BodyExt; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; use crate::request::{Request, RequestExt}; diff --git a/cot/src/form/field_value.rs b/cot/src/form/field_value.rs index 04c31c1b5..57315379a 100644 --- a/cot/src/form/field_value.rs +++ b/cot/src/form/field_value.rs @@ -4,7 +4,7 @@ use std::fmt::Display; use bytes::Bytes; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; /// A value from a form field. /// diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 54e9e42e3..acf62ab5d 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -96,6 +96,8 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; pub use body::Body; +#[doc(inline)] +pub use cot_core::error::Error; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// @@ -159,7 +161,6 @@ pub use cot_macros::e2e_test; /// ``` pub use cot_macros::main; pub use cot_macros::test; -pub use error::error_impl::Error; #[cfg(feature = "openapi")] pub use schemars; pub use {bytes, http}; diff --git a/cot/src/project.rs b/cot/src/project.rs index c4014cb00..1eaf638b8 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -51,9 +51,8 @@ use crate::config::{AuthBackendConfig, ProjectConfig}; use crate::db::Database; #[cfg(feature = "db")] use crate::db::migrations::{MigrationEngine, SyncDynMigration}; -use crate::error::UncaughtPanic; -use crate::error::error_impl::impl_into_cot_error; use crate::error::handler::{DynErrorPageHandler, RequestOuterError}; +use crate::error::{UncaughtPanic, impl_into_cot_error}; use crate::error_page::Diagnostics; use crate::handler::BoxedHandler; use crate::html::Html; diff --git a/cot/src/request.rs b/cot/src/request.rs index 687be8391..3122b8124 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -20,7 +20,7 @@ use indexmap::IndexMap; #[cfg(feature = "db")] use crate::db::Database; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::request::extractors::FromRequestHead; use crate::router::Router; use crate::{Body, Result}; diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 51672e4de..59428b5f2 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -553,7 +553,7 @@ impl FromRequestHead for Auth { /// ``` pub use cot_macros::FromRequestHead; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; #[cfg(test)] mod tests { diff --git a/cot/src/response/into_response.rs b/cot/src/response/into_response.rs index 1f1d1f909..8fa87cf70 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,5 +1,5 @@ use bytes::{Bytes, BytesMut}; -use cot::error::error_impl::impl_into_cot_error; +use cot::error::impl_into_cot_error; use cot::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; use cot::response::Response; use cot::{Body, Error, StatusCode}; diff --git a/cot/src/router.rs b/cot/src/router.rs index 9c6eed37e..f49b8891b 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -30,8 +30,7 @@ use std::task::{Context, Poll}; use derive_more::with_trait::Debug; use tracing::debug; -use crate::error::NotFound; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::{NotFound, impl_into_cot_error}; use crate::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; use crate::request::{AppName, PathParams, Request, RequestExt, RequestHead, RouteName}; use crate::response::Response; diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index 3b2e35d13..4742f3be2 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -10,7 +10,7 @@ use std::fmt::Display; use thiserror::Error; use tracing::debug; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; #[derive(Debug, Clone)] pub(super) struct PathMatcher { diff --git a/cot/src/static_files.rs b/cot/src/static_files.rs index 7e7a8d962..5fadcdade 100644 --- a/cot/src/static_files.rs +++ b/cot/src/static_files.rs @@ -21,7 +21,7 @@ use tower::Service; use crate::Body; use crate::config::{StaticFilesConfig, StaticFilesPathRewriteMode}; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::project::MiddlewareContext; use crate::response::{Response, ResponseExt}; diff --git a/cot/src/test.rs b/cot/src/test.rs index dac987e11..5f6818717 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -38,7 +38,7 @@ use crate::db::migrations::{ DynMigration, MigrationDependency, MigrationEngine, MigrationWrapper, Operation, }; #[cfg(feature = "redis")] -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::handler::BoxedHandler; use crate::project::{prepare_request, prepare_request_for_error_handler, run_at_with_shutdown}; use crate::request::Request; From a6dd5f8118e57cefeb89a2545730edc5c47e30d0 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 18:10:22 +0100 Subject: [PATCH 04/14] move body --- Cargo.lock | 10 ++++++++++ cot-core/Cargo.toml | 12 ++++++++++++ {cot => cot-core}/src/body.rs | 11 ++++------- cot-core/src/lib.rs | 6 ++++++ cot/src/lib.rs | 16 +++++++--------- 5 files changed, 39 insertions(+), 16 deletions(-) rename {cot => cot-core}/src/body.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index b64eb8aea..603efd10c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,12 +795,22 @@ name = "cot_core" version = "0.4.0" dependencies = [ "askama", + "async-stream", + "axum", "backtrace", + "bytes", + "cot", "derive_more", + "futures", + "futures-core", "http 1.4.0", + "http-body", + "http-body-util", "serde", "serde_json", + "sync_wrapper", "thiserror 2.0.17", + "tokio", "tower-sessions", ] diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 6f8d083ef..02f9e8286 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -22,7 +22,19 @@ thiserror.workspace = true serde.workspace = true serde_json.workspace = true backtrace.workspace = true +bytes.workspace = true +futures-core.workspace = true +http-body.workspace = true +http-body-util.workspace = true +sync_wrapper.workspace = true +axum.workspace = true # Trait implementations askama.workspace = true tower-sessions.workspace = true + +[dev-dependencies] +async-stream.workspace = true +cot = { workspace = true, features = ["test"] } +futures.workspace = true +tokio.workspace = true diff --git a/cot/src/body.rs b/cot-core/src/body.rs similarity index 97% rename from cot/src/body.rs rename to cot-core/src/body.rs index fafedaaf7..e576d44e2 100644 --- a/cot/src/body.rs +++ b/cot-core/src/body.rs @@ -28,10 +28,10 @@ use crate::{Error, Result}; /// ``` #[derive(Debug)] pub struct Body { - pub(crate) inner: BodyInner, + pub inner: BodyInner, } -pub(crate) enum BodyInner { +pub enum BodyInner { Fixed(Bytes), Streaming(SyncWrapper> + Send>>>), Axum(SyncWrapper), @@ -166,12 +166,12 @@ impl Body { } #[must_use] - pub(crate) fn axum(inner: axum::body::Body) -> Self { + pub fn axum(inner: axum::body::Body) -> Self { Self::new(BodyInner::Axum(SyncWrapper::new(inner))) } #[must_use] - pub(crate) fn wrapper(inner: BoxBody) -> Self { + pub fn wrapper(inner: BoxBody) -> Self { Self::new(BodyInner::Wrapper(inner)) } } @@ -261,9 +261,6 @@ impl_into_cot_error!(ReadRequestBody, BAD_REQUEST); #[cfg(test)] mod tests { - use std::pin::Pin; - use std::task::{Context, Poll}; - use futures::stream; use http_body::Body as HttpBody; diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index becc30880..98b8d38c7 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,4 +1,7 @@ +mod body; + pub mod error; +pub use body::{Body, BodyInner}; pub use error::Error; /// A type alias for an HTTP status code. @@ -6,3 +9,6 @@ pub type StatusCode = http::StatusCode; /// A type alias for an HTTP method. pub type Method = http::Method; + +/// A type alias for a result that can return a [`Error`]. +pub type Result = std::result::Result; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index acf62ab5d..57c84acd8 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -69,7 +69,6 @@ mod headers; pub mod __private; pub mod admin; pub mod auth; -mod body; pub mod cli; pub mod common_types; pub mod config; @@ -93,11 +92,16 @@ pub mod static_files; pub mod test; pub(crate) mod utils; +pub(crate) mod body { + #[doc(inline)] + #[expect(unused_imports)] + pub(crate) use cot_core::BodyInner; +} + #[cfg(feature = "openapi")] pub use aide; -pub use body::Body; #[doc(inline)] -pub use cot_core::error::Error; +pub use cot_core::{Body, Method, Result, StatusCode, error::Error}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// @@ -169,9 +173,3 @@ pub use crate::handler::{BoxedHandler, RequestHandler}; pub use crate::project::{ App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, }; - -/// A type alias for a result that can return a [`cot::Error`]. -pub type Result = std::result::Result; - -#[doc(inline)] -pub use cot_core::{Method, StatusCode}; From 9dee0aeb42a7061a5f8f37a34b32a55c350c28c6 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 18:23:16 +0100 Subject: [PATCH 05/14] move headers --- cot-core/Cargo.toml | 4 ++++ cot-core/src/headers.rs | 7 +++++++ cot-core/src/lib.rs | 2 ++ cot/Cargo.toml | 2 +- cot/src/headers.rs | 7 ------- cot/src/lib.rs | 3 ++- 6 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 cot-core/src/headers.rs delete mode 100644 cot/src/headers.rs diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 02f9e8286..838115f82 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -38,3 +38,7 @@ async-stream.workspace = true cot = { workspace = true, features = ["test"] } futures.workspace = true tokio.workspace = true + +[features] +default = [] +json = [] diff --git a/cot-core/src/headers.rs b/cot-core/src/headers.rs new file mode 100644 index 000000000..746790b2e --- /dev/null +++ b/cot-core/src/headers.rs @@ -0,0 +1,7 @@ +pub const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8"; +pub const MULTIPART_FORM_CONTENT_TYPE: &str = "multipart/form-data"; +pub const URLENCODED_FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; +#[cfg(feature = "json")] +pub const JSON_CONTENT_TYPE: &str = "application/json"; +pub const PLAIN_TEXT_CONTENT_TYPE: &str = "text/plain; charset=utf-8"; +pub const OCTET_STREAM_CONTENT_TYPE: &str = "application/octet-stream"; diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index 98b8d38c7..f3fb94872 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,6 +1,8 @@ mod body; pub mod error; +pub mod headers; + pub use body::{Body, BodyInner}; pub use error::Error; diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 2823cbb1f..7894e66fe 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -111,7 +111,7 @@ sqlite = ["db", "sea-query/backend-sqlite", "sea-query-binder/sqlx-sqlite", "sql postgres = ["db", "sea-query/backend-postgres", "sea-query-binder/sqlx-postgres", "sqlx/postgres"] mysql = ["db", "sea-query/backend-mysql", "sea-query-binder/sqlx-mysql", "sqlx/mysql"] redis = ["cache", "dep:deadpool-redis", "dep:redis", "json"] -json = ["dep:serde_json"] +json = ["dep:serde_json", "cot_core/json"] openapi = ["json", "dep:aide", "dep:schemars"] swagger-ui = ["openapi", "dep:swagger-ui-redist"] live-reload = ["dep:tower-livereload"] diff --git a/cot/src/headers.rs b/cot/src/headers.rs deleted file mode 100644 index 35a1a8473..000000000 --- a/cot/src/headers.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub(crate) const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8"; -pub(crate) const MULTIPART_FORM_CONTENT_TYPE: &str = "multipart/form-data"; -pub(crate) const URLENCODED_FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; -#[cfg(feature = "json")] -pub(crate) const JSON_CONTENT_TYPE: &str = "application/json"; -pub(crate) const PLAIN_TEXT_CONTENT_TYPE: &str = "text/plain; charset=utf-8"; -pub(crate) const OCTET_STREAM_CONTENT_TYPE: &str = "application/octet-stream"; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 57c84acd8..6087c0dac 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -62,7 +62,6 @@ pub mod db; /// including 404 Not Found errors, uncaught panics, and custom error pages. pub mod error; pub mod form; -mod headers; // Not public API. Referenced by macro-generated code. #[doc(hidden)] #[path = "private.rs"] @@ -101,6 +100,8 @@ pub(crate) mod body { #[cfg(feature = "openapi")] pub use aide; #[doc(inline)] +pub(crate) use cot_core::headers; +#[doc(inline)] pub use cot_core::{Body, Method, Result, StatusCode, error::Error}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. From 2da6c5dba9e84284ed61294661972e95f8cb9e5f Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 18:31:34 +0100 Subject: [PATCH 06/14] move html --- cot-core/Cargo.toml | 2 -- {cot => cot-core}/src/html.rs | 0 cot-core/src/lib.rs | 1 + cot/src/lib.rs | 3 +-- 4 files changed, 2 insertions(+), 4 deletions(-) rename {cot => cot-core}/src/html.rs (100%) diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 838115f82..b4eb0a7a7 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -28,8 +28,6 @@ http-body.workspace = true http-body-util.workspace = true sync_wrapper.workspace = true axum.workspace = true - -# Trait implementations askama.workspace = true tower-sessions.workspace = true diff --git a/cot/src/html.rs b/cot-core/src/html.rs similarity index 100% rename from cot/src/html.rs rename to cot-core/src/html.rs diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index f3fb94872..ed4cb5170 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -2,6 +2,7 @@ mod body; pub mod error; pub mod headers; +pub mod html; pub use body::{Body, BodyInner}; pub use error::Error; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 6087c0dac..fce1c5c07 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -74,7 +74,6 @@ pub mod config; mod error_page; #[macro_use] pub(crate) mod handler; -pub mod html; #[cfg(feature = "json")] pub mod json; pub mod middleware; @@ -102,7 +101,7 @@ pub use aide; #[doc(inline)] pub(crate) use cot_core::headers; #[doc(inline)] -pub use cot_core::{Body, Method, Result, StatusCode, error::Error}; +pub use cot_core::{Body, Method, Result, StatusCode, error::Error, html}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// From 725f180e28ecf55fd8e5299633c6d6747a43b912 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 18:38:32 +0100 Subject: [PATCH 07/14] move json --- {cot => cot-core}/src/json.rs | 0 cot-core/src/lib.rs | 2 ++ cot/src/lib.rs | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) rename {cot => cot-core}/src/json.rs (100%) diff --git a/cot/src/json.rs b/cot-core/src/json.rs similarity index 100% rename from cot/src/json.rs rename to cot-core/src/json.rs diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index ed4cb5170..5906d3873 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -3,6 +3,8 @@ mod body; pub mod error; pub mod headers; pub mod html; +#[cfg(feature = "json")] +pub mod json; pub use body::{Body, BodyInner}; pub use error::Error; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index fce1c5c07..daf7d21cc 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -74,8 +74,6 @@ pub mod config; mod error_page; #[macro_use] pub(crate) mod handler; -#[cfg(feature = "json")] -pub mod json; pub mod middleware; #[cfg(feature = "openapi")] pub mod openapi; @@ -100,6 +98,9 @@ pub(crate) mod body { pub use aide; #[doc(inline)] pub(crate) use cot_core::headers; +#[cfg(feature = "json")] +#[doc(inline)] +pub use cot_core::json; #[doc(inline)] pub use cot_core::{Body, Method, Result, StatusCode, error::Error, html}; /// An attribute macro that defines an end-to-end test function for a From a4fa1500aa3223f04c0bc1c6889a0ff45b8ecd92 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 18:41:17 +0100 Subject: [PATCH 08/14] move response --- Cargo.lock | 2 + cot-core/Cargo.toml | 2 + cot-core/src/error/handler.rs | 302 ------------------ cot-core/src/lib.rs | 1 + {cot => cot-core}/src/response.rs | 0 .../src/response/into_response.rs | 66 ++-- cot/src/lib.rs | 9 +- 7 files changed, 39 insertions(+), 343 deletions(-) delete mode 100644 cot-core/src/error/handler.rs rename {cot => cot-core}/src/response.rs (100%) rename {cot => cot-core}/src/response/into_response.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index 603efd10c..a21ba7f18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -800,6 +800,7 @@ dependencies = [ "backtrace", "bytes", "cot", + "cot_macros", "derive_more", "futures", "futures-core", @@ -808,6 +809,7 @@ dependencies = [ "http-body-util", "serde", "serde_json", + "serde_path_to_error", "sync_wrapper", "thiserror 2.0.17", "tokio", diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index b4eb0a7a7..a57475682 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -28,8 +28,10 @@ http-body.workspace = true http-body-util.workspace = true sync_wrapper.workspace = true axum.workspace = true +cot_macros.workspace = true askama.workspace = true tower-sessions.workspace = true +serde_path_to_error.workspace = true [dev-dependencies] async-stream.workspace = true diff --git a/cot-core/src/error/handler.rs b/cot-core/src/error/handler.rs deleted file mode 100644 index 8dd03002e..000000000 --- a/cot-core/src/error/handler.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! Error handling functionality for custom error pages and handlers. - -use std::fmt::Display; -use std::marker::PhantomData; -use std::ops::Deref; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; - -use derive_more::with_trait::Debug; - -use crate::Error; -use crate::handler::handle_all_parameters; -use crate::request::extractors::FromRequestHead; -use crate::request::{Request, RequestHead}; -use crate::response::Response; - -/// A trait for handling error pages in Cot applications. -/// -/// This trait is implemented by functions that can handle error pages. The -/// trait is automatically implemented for async functions that take parameters -/// implementing [`FromRequestHead`] and return a type that implements -/// [`IntoResponse`]. -/// -/// # Examples -/// -/// ``` -/// use cot::Project; -/// use cot::error::handler::{DynErrorPageHandler, RequestError}; -/// use cot::html::Html; -/// use cot::response::IntoResponse; -/// -/// struct MyProject; -/// impl Project for MyProject { -/// fn error_handler(&self) -> DynErrorPageHandler { -/// DynErrorPageHandler::new(error_handler) -/// } -/// } -/// -/// // This function automatically implements ErrorPageHandler -/// async fn error_handler(error: RequestError) -> impl IntoResponse { -/// Html::new(format!("An error occurred: {error}")).with_status(error.status_code()) -/// } -/// ``` -#[diagnostic::on_unimplemented( - message = "`{Self}` is not a valid error page handler", - label = "not a valid error page handler", - note = "make sure the function is marked `async`", - note = "make sure all parameters implement `FromRequestHead`", - note = "make sure the function takes no more than 10 parameters", - note = "make sure the function returns a type that implements `IntoResponse`" -)] -pub trait ErrorPageHandler { - /// Handles an error request and returns a response. - /// - /// This method is called when an error occurs and the application needs to - /// generate an error page response. - /// - /// # Errors - /// - /// This method may return an error if the handler fails to build a - /// response. In this case, the error will be logged and a generic - /// error page will be returned to the user. - fn handle(&self, head: &RequestHead) -> impl Future> + Send; -} - -pub(crate) trait BoxErrorPageHandler: Send + Sync { - fn handle<'a>( - &'a self, - head: &'a RequestHead, - ) -> Pin> + Send + 'a>>; -} - -/// A type-erased wrapper around an error page handler. -/// -/// This struct allows storing different types of error page handlers in a -/// homogeneous collection or service. It implements [`Clone`] and can be -/// used with Cot's error handling infrastructure. -#[derive(Debug, Clone)] -pub struct DynErrorPageHandler { - #[debug("..")] - handler: Arc, -} - -impl DynErrorPageHandler { - /// Creates a new `DynErrorPageHandler` from a concrete error page handler. - /// - /// This method wraps a concrete error page handler in a type-erased - /// wrapper, allowing it to be used in - /// [`crate::project::Project::error_handler`]. - /// - /// # Examples - /// - /// ``` - /// use cot::Project; - /// use cot::error::handler::{DynErrorPageHandler, RequestError}; - /// use cot::html::Html; - /// use cot::response::IntoResponse; - /// - /// struct MyProject; - /// impl Project for MyProject { - /// fn error_handler(&self) -> DynErrorPageHandler { - /// DynErrorPageHandler::new(error_handler) - /// } - /// } - /// - /// // This function automatically implements ErrorPageHandler - /// async fn error_handler(error: RequestError) -> impl IntoResponse { - /// Html::new(format!("An error occurred: {error}")).with_status(error.status_code()) - /// } - /// ``` - pub fn new(handler: H) -> Self - where - HandlerParams: 'static, - H: ErrorPageHandler + Send + Sync + 'static, - { - struct Inner(H, PhantomData T>); - - impl + Send + Sync> BoxErrorPageHandler for Inner { - fn handle<'a>( - &'a self, - head: &'a RequestHead, - ) -> Pin> + Send + 'a>> { - Box::pin(self.0.handle(head)) - } - } - - Self { - handler: Arc::new(Inner(handler, PhantomData)), - } - } -} - -impl tower::Service for DynErrorPageHandler { - type Response = Response; - type Error = Error; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - let handler = self.handler.clone(); - let (head, _) = req.into_parts(); - Box::pin(async move { handler.handle(&head).await }) - } -} - -macro_rules! impl_request_handler { - ($($ty:ident),*) => { - impl ErrorPageHandler<($($ty,)*)> for Func - where - Func: FnOnce($($ty,)*) -> Fut + Clone + Send + Sync + 'static, - $($ty: FromRequestHead + Send,)* - Fut: Future + Send, - R: crate::response::IntoResponse, - { - #[allow( - clippy::allow_attributes, - non_snake_case, - unused_variables, - reason = "for the case where there are no params" - )] - async fn handle(&self, head: &RequestHead) -> crate::Result { - $( - let $ty = <$ty as FromRequestHead>::from_request_head(&head).await?; - )* - - self.clone()($($ty,)*).await.into_response() - } - } - }; -} - -handle_all_parameters!(impl_request_handler); - -/// A wrapper around [`Error`] that contains an error (outermost, -/// possibly middleware-wrapped) to be processed by the error handler. -/// -/// This returns the outermost error returned by the request handler and -/// middlewares. In most cases, you should use [`RequestError`] instead, which -/// dereferences to the inner [`Error`] (the first error in the error chain that -/// contains an explicitly set status code). [`RequestError`] will allow you to -/// check for specific error types even when middleware might have wrapped the -/// error. -#[derive(Debug, Clone)] -pub struct RequestOuterError(Arc); - -impl RequestOuterError { - #[must_use] - pub(crate) fn new(error: Error) -> Self { - Self(Arc::new(error)) - } -} - -impl Deref for RequestOuterError { - type Target = Error; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for RequestOuterError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.0.inner(), f) - } -} - -impl FromRequestHead for RequestOuterError { - async fn from_request_head(head: &RequestHead) -> crate::Result { - let error = head.extensions.get::(); - error - .ok_or_else(|| { - Error::internal("No error found in request head. Make sure you use this extractor in an error handler.") - }).cloned() - } -} - -/// A wrapper around [`Error`] that contains an error to be processed by the -/// error handler. -/// -/// Note that the [`Deref`] implementation returns the inner [`Error`] (see -/// [`Error::inner`]), which is the first error in the error chain that contains -/// an explicitly set status code. This is usually what you want since it allows -/// you to check for specific error types even when middleware might have -/// wrapped the error. If you need to access the outermost error instead, -/// you can use [`RequestOuterError`]. -#[derive(Debug, Clone)] -pub struct RequestError(Arc); - -impl Deref for RequestError { - type Target = Error; - - fn deref(&self) -> &Self::Target { - self.0.inner() - } -} - -impl Display for RequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.0.inner(), f) - } -} - -impl FromRequestHead for RequestError { - async fn from_request_head(head: &RequestHead) -> crate::Result { - let error = head.extensions.get::(); - error - .ok_or_else(|| { - Error::internal( - "No error found in request head. \ - Make sure you use this extractor in an error handler.", - ) - }) - .map(|request_error| Self(request_error.0.clone())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn request_outer_error_display() { - let error = Error::internal("Test error"); - let request_error = RequestOuterError::new(error); - - assert_eq!(format!("{request_error}"), "Test error"); - } - - #[test] - fn request_error_display() { - let error = Error::internal("Test error"); - let request_error = RequestError(Arc::new(error)); - - assert_eq!(format!("{request_error}"), "Test error"); - } - - #[cot::test] - async fn request_outer_error_from_request_head() { - let request = Request::default(); - let (mut head, _) = request.into_parts(); - head.extensions - .insert(RequestOuterError::new(Error::internal("Test error"))); - - let extracted_error = RequestOuterError::from_request_head(&head).await.unwrap(); - assert_eq!(format!("{extracted_error}"), "Test error"); - } - - #[cot::test] - async fn request_error_from_request_head() { - let request = Request::default(); - let (mut head, _) = request.into_parts(); - head.extensions - .insert(RequestOuterError::new(Error::internal("Test error"))); - - let extracted_error = RequestError::from_request_head(&head).await.unwrap(); - assert_eq!(format!("{extracted_error}"), "Test error"); - } -} diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index 5906d3873..0247d6fe2 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -5,6 +5,7 @@ pub mod headers; pub mod html; #[cfg(feature = "json")] pub mod json; +pub mod response; pub use body::{Body, BodyInner}; pub use error::Error; diff --git a/cot/src/response.rs b/cot-core/src/response.rs similarity index 100% rename from cot/src/response.rs rename to cot-core/src/response.rs diff --git a/cot/src/response/into_response.rs b/cot-core/src/response/into_response.rs similarity index 92% rename from cot/src/response/into_response.rs rename to cot-core/src/response/into_response.rs index 8fa87cf70..499af91cb 100644 --- a/cot/src/response/into_response.rs +++ b/cot-core/src/response/into_response.rs @@ -1,13 +1,13 @@ use bytes::{Bytes, BytesMut}; -use cot::error::impl_into_cot_error; -use cot::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; -use cot::response::Response; -use cot::{Body, Error, StatusCode}; use http; +use crate::error::impl_into_cot_error; #[cfg(feature = "json")] use crate::headers::JSON_CONTENT_TYPE; +use crate::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; use crate::html::Html; +use crate::response::Response; +use crate::{Body, Error, StatusCode}; /// Trait for generating responses. /// Types that implement `IntoResponse` can be returned from handlers. @@ -20,11 +20,11 @@ use crate::html::Html; /// However, it might be necessary if you have a custom error type that you want /// to return from handlers. pub trait IntoResponse { - /// Converts the implementing type into a `cot::Result`. + /// Converts the implementing type into a `crate::Result`. /// /// # Errors /// Returns an error if the conversion fails. - fn into_response(self) -> cot::Result; + fn into_response(self) -> crate::Result; /// Modifies the response by appending the specified header. /// @@ -112,7 +112,7 @@ pub struct WithHeader { } impl IntoResponse for WithHeader { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { if let Some((key, value)) = self.header { resp.headers_mut().append(key, value); @@ -130,7 +130,7 @@ pub struct WithContentType { } impl IntoResponse for WithContentType { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { if let Some(content_type) = self.content_type { resp.headers_mut() @@ -149,7 +149,7 @@ pub struct WithStatus { } impl IntoResponse for WithStatus { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { *resp.status_mut() = self.status; resp @@ -165,7 +165,7 @@ pub struct WithBody { } impl IntoResponse for WithBody { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { *resp.body_mut() = self.body; resp @@ -185,7 +185,7 @@ where T: IntoResponse, D: Clone + Send + Sync + 'static, { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { resp.extensions_mut().insert(self.extension); resp @@ -196,7 +196,7 @@ where macro_rules! impl_into_response_for_type_and_mime { ($ty:ty, $mime:expr) => { impl IntoResponse for $ty { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Body::from(self) .with_header(http::header::CONTENT_TYPE, $mime) .into_response() @@ -208,7 +208,7 @@ macro_rules! impl_into_response_for_type_and_mime { // General implementations impl IntoResponse for () { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Body::empty().into_response() } } @@ -218,7 +218,7 @@ where R: IntoResponse, E: Into, { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { match self { Ok(value) => value.into_response(), Err(err) => Err(err.into()), @@ -227,13 +227,13 @@ where } impl IntoResponse for Error { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Err(self) } } impl IntoResponse for Response { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Ok(self) } } @@ -244,7 +244,7 @@ impl_into_response_for_type_and_mime!(&'static str, PLAIN_TEXT_CONTENT_TYPE); impl_into_response_for_type_and_mime!(String, PLAIN_TEXT_CONTENT_TYPE); impl IntoResponse for Box { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { String::from(self).into_response() } } @@ -256,25 +256,25 @@ impl_into_response_for_type_and_mime!(Vec, OCTET_STREAM_CONTENT_TYPE); impl_into_response_for_type_and_mime!(Bytes, OCTET_STREAM_CONTENT_TYPE); impl IntoResponse for &'static [u8; N] { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.as_slice().into_response() } } impl IntoResponse for [u8; N] { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.to_vec().into_response() } } impl IntoResponse for Box<[u8]> { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Vec::from(self).into_response() } } impl IntoResponse for BytesMut { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.freeze().into_response() } } @@ -282,13 +282,13 @@ impl IntoResponse for BytesMut { // HTTP structures for common uses impl IntoResponse for StatusCode { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { ().into_response().with_status(self).into_response() } } impl IntoResponse for http::HeaderMap { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { ().into_response().map(|mut resp| { *resp.headers_mut() = self; resp @@ -297,7 +297,7 @@ impl IntoResponse for http::HeaderMap { } impl IntoResponse for http::Extensions { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { ().into_response().map(|mut resp| { *resp.extensions_mut() = self; resp @@ -306,7 +306,7 @@ impl IntoResponse for http::Extensions { } impl IntoResponse for crate::response::ResponseHead { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Ok(Response::from_parts(self, Body::empty())) } } @@ -329,7 +329,7 @@ impl IntoResponse for Html { /// /// let response = html.into_response(); /// ``` - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.0 .into_response() .with_content_type(HTML_CONTENT_TYPE) @@ -338,7 +338,7 @@ impl IntoResponse for Html { } #[cfg(feature = "json")] -impl IntoResponse for cot::json::Json { +impl IntoResponse for crate::json::Json { /// Create a new JSON response. /// /// This creates a new [`Response`] object with a content type of @@ -357,7 +357,7 @@ impl IntoResponse for cot::json::Json { /// /// let response = json.into_response(); /// ``` - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { // a "reasonable default" for a JSON response size const DEFAULT_JSON_SIZE: usize = 128; @@ -380,7 +380,7 @@ impl_into_cot_error!(JsonSerializeError, INTERNAL_SERVER_ERROR); // Shortcuts for common uses impl IntoResponse for Body { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Ok(Response::new(self)) } } @@ -388,13 +388,13 @@ impl IntoResponse for Body { #[cfg(test)] mod tests { use bytes::{Bytes, BytesMut}; - use cot::response::Response; - use cot::{Body, StatusCode}; use http::{self, HeaderMap, HeaderValue}; use super::*; use crate::error::NotFound; use crate::html::Html; + use crate::response::Response; + use crate::{Body, StatusCode}; #[cot::test] async fn test_unit_into_response() { @@ -780,7 +780,7 @@ mod tests { name: "test".to_string(), value: 123, }; - let json = cot::json::Json(data); + let json = crate::json::Json(data); let response = json.into_response().unwrap(); assert_eq!(response.status(), StatusCode::OK); @@ -801,7 +801,7 @@ mod tests { use std::collections::HashMap; let data = HashMap::from([("key", "value")]); - let json = cot::json::Json(data); + let json = crate::json::Json(data); let response = json.into_response().unwrap(); assert_eq!(response.status(), StatusCode::OK); diff --git a/cot/src/lib.rs b/cot/src/lib.rs index daf7d21cc..5c0d2863e 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -79,7 +79,6 @@ pub mod middleware; pub mod openapi; pub mod project; pub mod request; -pub mod response; pub mod router; mod serializers; pub mod session; @@ -88,12 +87,6 @@ pub mod static_files; pub mod test; pub(crate) mod utils; -pub(crate) mod body { - #[doc(inline)] - #[expect(unused_imports)] - pub(crate) use cot_core::BodyInner; -} - #[cfg(feature = "openapi")] pub use aide; #[doc(inline)] @@ -102,7 +95,7 @@ pub(crate) use cot_core::headers; #[doc(inline)] pub use cot_core::json; #[doc(inline)] -pub use cot_core::{Body, Method, Result, StatusCode, error::Error, html}; +pub use cot_core::{Body, Method, Result, StatusCode, error::Error, html, response}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// From 2994eca9632499e74826b9af7da0276ec92c09af Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 19:35:06 +0100 Subject: [PATCH 09/14] move request --- Cargo.lock | 3 + cot-core/Cargo.toml | 3 + cot-core/src/lib.rs | 1 + cot-core/src/request.rs | 328 ++++++++++++ cot-core/src/request/extractors.rs | 503 ++++++++++++++++++ .../src/request/path_params_deserializer.rs | 0 cot/src/handler.rs | 8 +- cot/src/request.rs | 308 +---------- cot/src/request/extractors.rs | 477 +---------------- 9 files changed, 874 insertions(+), 757 deletions(-) create mode 100644 cot-core/src/request.rs create mode 100644 cot-core/src/request/extractors.rs rename {cot => cot-core}/src/request/path_params_deserializer.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index a21ba7f18..289d1857d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -802,12 +802,15 @@ dependencies = [ "cot", "cot_macros", "derive_more", + "form_urlencoded", "futures", "futures-core", "http 1.4.0", "http-body", "http-body-util", + "indexmap", "serde", + "serde_html_form", "serde_json", "serde_path_to_error", "sync_wrapper", diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index a57475682..96a4bf6a1 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -32,6 +32,9 @@ cot_macros.workspace = true askama.workspace = true tower-sessions.workspace = true serde_path_to_error.workspace = true +indexmap.workspace = true +serde_html_form.workspace = true +form_urlencoded.workspace = true [dev-dependencies] async-stream.workspace = true diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index 0247d6fe2..fa01c3b80 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -5,6 +5,7 @@ pub mod headers; pub mod html; #[cfg(feature = "json")] pub mod json; +pub mod request; pub mod response; pub use body::{Body, BodyInner}; diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs new file mode 100644 index 000000000..ba91effcb --- /dev/null +++ b/cot-core/src/request.rs @@ -0,0 +1,328 @@ +//! HTTP request type and helper methods. +//! +//! Cot uses the [`Request`](http::Request) type from the [`http`] crate +//! to represent incoming HTTP requests. However, it also provides a +//! [`RequestExt`] trait that contain various helper methods for working with +//! HTTP requests. These methods are used to access the application context, +//! project configuration, path parameters, and more. You probably want to have +//! a `use` statement for [`RequestExt`] in your code most of the time to be +//! able to use these functions: +//! +//! ``` +//! use cot::request::RequestExt; +//! ``` + +use indexmap::IndexMap; + +use crate::Body; +#[cfg(feature = "db")] +use crate::db::Database; +use crate::error::impl_into_cot_error; + +pub mod extractors; +mod path_params_deserializer; + +/// HTTP request type. +pub type Request = http::Request; + +/// HTTP request head type. +pub type RequestHead = http::request::Parts; + +#[derive(Debug, thiserror::Error)] +#[error("invalid content type; expected `{expected}`, found `{actual}`")] +pub struct InvalidContentType { + pub expected: &'static str, + pub actual: String, +} +impl_into_cot_error!(InvalidContentType, BAD_REQUEST); + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppName(pub String); + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RouteName(pub String); + +/// Path parameters extracted from the request URL, and available as a map of +/// strings. +/// +/// This struct is meant to be mainly used using the [`PathParams::parse`] +/// method, which will deserialize the path parameters into a type `T` +/// implementing `serde::DeserializeOwned`. If needed, you can also access the +/// path parameters directly using the [`PathParams::get`] method. +/// +/// # Examples +/// +/// ``` +/// use cot::request::{PathParams, Request, RequestExt}; +/// use cot::response::Response; +/// use cot::test::TestRequestBuilder; +/// +/// async fn my_handler(mut request: Request) -> cot::Result { +/// let path_params = request.path_params(); +/// let name = path_params.get("name").unwrap(); +/// +/// // using more ergonomic syntax: +/// let name: String = request.path_params().parse()?; +/// +/// let name = println!("Hello, {}!", name); +/// // ... +/// # unimplemented!() +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct PathParams { + params: IndexMap, +} + +impl Default for PathParams { + fn default() -> Self { + Self::new() + } +} + +impl PathParams { + /// Creates a new [`PathParams`] instance. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get("name"), Some("world")); + /// ``` + #[must_use] + pub fn new() -> Self { + Self { + params: IndexMap::new(), + } + } + + /// Inserts a new path parameter. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get("name"), Some("world")); + /// ``` + pub fn insert(&mut self, name: String, value: String) { + self.params.insert(name, value); + } + + /// Iterates over the path parameters. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// for (name, value) in path_params.iter() { + /// println!("{}: {}", name, value); + /// } + /// ``` + pub fn iter(&self) -> impl Iterator { + self.params + .iter() + .map(|(name, value)| (name.as_str(), value.as_str())) + } + + /// Returns the number of path parameters. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let path_params = PathParams::new(); + /// assert_eq!(path_params.len(), 0); + /// ``` + #[must_use] + pub fn len(&self) -> usize { + self.params.len() + } + + /// Returns `true` if the path parameters are empty. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let path_params = PathParams::new(); + /// assert!(path_params.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.params.is_empty() + } + + /// Returns the value of a path parameter. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get("name"), Some("world")); + /// ``` + #[must_use] + pub fn get(&self, name: &str) -> Option<&str> { + self.params.get(name).map(String::as_str) + } + + /// Returns the value of a path parameter at the given index. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get_index(0), Some("world")); + /// ``` + #[must_use] + pub fn get_index(&self, index: usize) -> Option<&str> { + self.params + .get_index(index) + .map(|(_, value)| value.as_str()) + } + + /// Returns the key of a path parameter at the given index. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.key_at_index(0), Some("name")); + /// ``` + #[must_use] + pub fn key_at_index(&self, index: usize) -> Option<&str> { + self.params.get_index(index).map(|(key, _)| key.as_str()) + } + + /// Deserializes the path parameters into a type `T` implementing + /// `serde::DeserializeOwned`. + /// + /// # Errors + /// + /// Throws an error if the path parameters could not be deserialized. + /// + /// # Examples + /// + /// ``` + /// use cot::request::PathParams; + /// + /// # fn main() -> Result<(), cot::Error> { + /// let mut path_params = PathParams::new(); + /// path_params.insert("hello".into(), "world".into()); + /// + /// let hello: String = path_params.parse()?; + /// assert_eq!(hello, "world"); + /// # Ok(()) + /// # } + /// ``` + /// + /// ``` + /// use cot::request::PathParams; + /// + /// # fn main() -> Result<(), cot::Error> { + /// let mut path_params = PathParams::new(); + /// path_params.insert("hello".into(), "world".into()); + /// path_params.insert("name".into(), "john".into()); + /// + /// let (hello, name): (String, String) = path_params.parse()?; + /// assert_eq!(hello, "world"); + /// assert_eq!(name, "john"); + /// # Ok(()) + /// # } + /// ``` + /// + /// ``` + /// use cot::request::PathParams; + /// use serde::Deserialize; + /// + /// # fn main() -> Result<(), cot::Error> { + /// let mut path_params = PathParams::new(); + /// path_params.insert("hello".into(), "world".into()); + /// path_params.insert("name".into(), "john".into()); + /// + /// #[derive(Deserialize)] + /// struct Params { + /// hello: String, + /// name: String, + /// } + /// + /// let params: Params = path_params.parse()?; + /// assert_eq!(params.hello, "world"); + /// assert_eq!(params.name, "john"); + /// # Ok(()) + /// # } + /// ``` + pub fn parse<'de, T: serde::Deserialize<'de>>( + &'de self, + ) -> std::result::Result { + let deserializer = path_params_deserializer::PathParamsDeserializer::new(self); + serde_path_to_error::deserialize(deserializer).map_err(PathParamsDeserializerError) + } +} + +/// An error that occurs when deserializing path parameters. +#[derive(Debug, Clone, thiserror::Error)] +#[error("could not parse path parameters: {0}")] +pub struct PathParamsDeserializerError( + // A wrapper over the original deserializer error. The exact error reason + // shouldn't be useful to the user, hence we're not exposing it. + #[source] serde_path_to_error::Error, +); +impl_into_cot_error!(PathParamsDeserializerError, BAD_REQUEST); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_params() { + let mut path_params = PathParams::new(); + path_params.insert("name".into(), "world".into()); + + assert_eq!(path_params.get("name"), Some("world")); + assert_eq!(path_params.get("missing"), None); + } + + #[test] + fn path_params_parse() { + #[derive(Debug, PartialEq, Eq, serde::Deserialize)] + struct Params { + hello: String, + foo: String, + } + + let mut path_params = PathParams::new(); + path_params.insert("hello".into(), "world".into()); + path_params.insert("foo".into(), "bar".into()); + + let params: Params = path_params.parse().unwrap(); + assert_eq!( + params, + Params { + hello: "world".to_string(), + foo: "bar".to_string(), + } + ); + } +} diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs new file mode 100644 index 000000000..8e6f0ace7 --- /dev/null +++ b/cot-core/src/request/extractors.rs @@ -0,0 +1,503 @@ +//! Extractors for request data. +//! +//! An extractor is a function that extracts data from a request. The main +//! benefit of using an extractor is that it can be used directly as a parameter +//! in a route handler. +//! +//! An extractor implements either [`FromRequest`] or [`FromRequestHead`]. +//! There are two variants because the request body can only be read once, so it +//! needs to be read in the [`FromRequest`] implementation. Therefore, there can +//! only be one extractor that implements [`FromRequest`] per route handler. +//! +//! # Examples +//! +//! For example, the [`Path`] extractor is used to extract path parameters: +//! +//! ``` +//! use cot::html::Html; +//! use cot::request::extractors::{FromRequest, Path}; +//! use cot::request::{Request, RequestExt}; +//! use cot::router::{Route, Router}; +//! use cot::test::TestRequestBuilder; +//! +//! async fn my_handler(Path(my_param): Path) -> Html { +//! Html::new(format!("Hello {my_param}!")) +//! } +//! +//! # #[tokio::main] +//! # async fn main() -> cot::Result<()> { +//! let router = Router::with_urls([Route::with_handler_and_name( +//! "/{my_param}/", +//! my_handler, +//! "home", +//! )]); +//! let request = TestRequestBuilder::get("/world/") +//! .router(router.clone()) +//! .build(); +//! +//! assert_eq!( +//! router +//! .handle(request) +//! .await? +//! .into_body() +//! .into_bytes() +//! .await?, +//! "Hello world!" +//! ); +//! # Ok(()) +//! # } +//! ``` + +use std::future::Future; + +use serde::de::DeserializeOwned; + +#[cfg(feature = "json")] +use crate::json::Json; +use crate::request::{InvalidContentType, PathParams, Request, RequestHead}; +use crate::{Body, Method}; + +/// Trait for extractors that consume the request body. +/// +/// Extractors implementing this trait are used in route handlers that consume +/// the request body and therefore can only be used once per request. +/// +/// See [`crate::request::extractors`] documentation for more information about +/// extractors. +pub trait FromRequest: Sized { + /// Extracts data from the request. + /// + /// # Errors + /// + /// Throws an error if the extractor fails to extract the data from the + /// request. + fn from_request( + head: &RequestHead, + body: Body, + ) -> impl Future> + Send; +} + +impl FromRequest for Request { + async fn from_request(head: &RequestHead, body: Body) -> crate::Result { + Ok(Request::from_parts(head.clone(), body)) + } +} + +/// Trait for extractors that don't consume the request body. +/// +/// Extractors implementing this trait are used in route handlers that don't +/// consume the request and therefore can be used multiple times per request. +/// +/// If you need to consume the body of the request, use [`FromRequest`] instead. +/// +/// See [`crate::request::extractors`] documentation for more information about +/// extractors. +pub trait FromRequestHead: Sized { + /// Extracts data from the request head. + /// + /// # Errors + /// + /// Throws an error if the extractor fails to extract the data from the + /// request head. + fn from_request_head(head: &RequestHead) -> impl Future> + Send; +} + +/// An extractor that extracts data from the URL params. +/// +/// The extractor is generic over a type that implements +/// `serde::de::DeserializeOwned`. +/// +/// # Examples +/// +/// ``` +/// use cot::html::Html; +/// use cot::request::extractors::{FromRequest, Path}; +/// use cot::request::{Request, RequestExt}; +/// use cot::router::{Route, Router}; +/// use cot::test::TestRequestBuilder; +/// +/// async fn my_handler(Path(my_param): Path) -> Html { +/// Html::new(format!("Hello {my_param}!")) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// let router = Router::with_urls([Route::with_handler_and_name( +/// "/{my_param}/", +/// my_handler, +/// "home", +/// )]); +/// let request = TestRequestBuilder::get("/world/") +/// .router(router.clone()) +/// .build(); +/// +/// assert_eq!( +/// router +/// .handle(request) +/// .await? +/// .into_body() +/// .into_bytes() +/// .await?, +/// "Hello world!" +/// ); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Path(pub D); + +impl FromRequestHead for Path { + async fn from_request_head(head: &RequestHead) -> crate::Result { + let params = head + .extensions + .get::() + .expect("PathParams extension missing") + .parse()?; + Ok(Self(params)) + } +} + +/// An extractor that extracts data from the URL query parameters. +/// +/// The extractor is generic over a type that implements +/// `serde::de::DeserializeOwned`. +/// +/// # Example +/// +/// ``` +/// use cot::RequestHandler; +/// use cot::html::Html; +/// use cot::request::extractors::{FromRequest, UrlQuery}; +/// use cot::router::{Route, Router}; +/// use cot::test::TestRequestBuilder; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// struct MyQuery { +/// hello: String, +/// } +/// +/// async fn my_handler(UrlQuery(query): UrlQuery) -> Html { +/// Html::new(format!("Hello {}!", query.hello)) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// let request = TestRequestBuilder::get("/?hello=world").build(); +/// +/// assert_eq!( +/// my_handler +/// .handle(request) +/// .await? +/// .into_body() +/// .into_bytes() +/// .await?, +/// "Hello world!" +/// ); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct UrlQuery(pub T); + +impl FromRequestHead for UrlQuery +where + D: DeserializeOwned, +{ + async fn from_request_head(head: &RequestHead) -> crate::Result { + let query = head.uri.query().unwrap_or_default(); + + let deserializer = + serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + + let value = + serde_path_to_error::deserialize(deserializer).map_err(QueryParametersParseError)?; + + Ok(UrlQuery(value)) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("could not parse query parameters: {0}")] +struct QueryParametersParseError(serde_path_to_error::Error); +impl_into_cot_error!(QueryParametersParseError, BAD_REQUEST); + +/// Extractor that gets the request body as JSON and deserializes it into a type +/// `T` implementing `serde::de::DeserializeOwned`. +/// +/// The content type of the request must be `application/json`. +/// +/// # Errors +/// +/// Throws an error if the content type is not `application/json`. +/// Throws an error if the request body could not be read. +/// Throws an error if the request body could not be deserialized - either +/// because the JSON is invalid or because the deserialization to the target +/// structure failed. +/// +/// # Example +/// +/// ``` +/// use cot::RequestHandler; +/// use cot::json::Json; +/// use cot::test::TestRequestBuilder; +/// use serde::{Deserialize, Serialize}; +/// +/// #[derive(Serialize, Deserialize)] +/// struct MyData { +/// hello: String, +/// } +/// +/// async fn my_handler(Json(data): Json) -> Json { +/// Json(data) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// let request = TestRequestBuilder::get("/") +/// .json(&MyData { +/// hello: "world".to_string(), +/// }) +/// .build(); +/// +/// assert_eq!( +/// my_handler +/// .handle(request) +/// .await? +/// .into_body() +/// .into_bytes() +/// .await?, +/// "{\"hello\":\"world\"}" +/// ); +/// # Ok(()) +/// # } +/// ``` +#[cfg(feature = "json")] +impl FromRequest for Json { + async fn from_request(head: &RequestHead, body: Body) -> crate::Result { + let content_type = head + .headers + .get(http::header::CONTENT_TYPE) + .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); + if content_type != crate::headers::JSON_CONTENT_TYPE { + return Err(InvalidContentType { + expected: crate::headers::JSON_CONTENT_TYPE, + actual: content_type.into_owned(), + } + .into()); + } + + let bytes = body.into_bytes().await?; + + let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); + let result = + serde_path_to_error::deserialize(deserializer).map_err(JsonDeserializeError)?; + + Ok(Self(result)) + } +} + +#[cfg(feature = "json")] +#[derive(Debug, thiserror::Error)] +#[error("JSON deserialization error: {0}")] +struct JsonDeserializeError(serde_path_to_error::Error); +#[cfg(feature = "json")] +impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); + +// extractor impls for existing types +impl FromRequestHead for RequestHead { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(head.clone()) + } +} + +impl FromRequestHead for Method { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(head.method.clone()) + } +} + +/// A derive macro that automatically implements the [`FromRequestHead`] trait +/// for structs. +/// +/// This macro generates code to extract each field of the struct from HTTP +/// request head, making it easy to create composite extractors that combine +/// multiple data sources from an incoming request. +/// +/// The macro works by calling [`FromRequestHead::from_request_head`] on each +/// field's type, allowing you to compose extractors seamlessly. All fields must +/// implement the [`FromRequestHead`] trait for the derivation to work. +/// +/// # Requirements +/// +/// - The target struct must have all fields implement [`FromRequestHead`] +/// - Works with named fields, unnamed fields (tuple structs), and unit structs +/// - The struct must be accessible where the macro is used +/// +/// # Examples +/// +/// ## Named Fields +/// +/// ```no_run +/// use cot::request::extractors::{Path, StaticFiles, UrlQuery}; +/// use cot::router::Urls; +/// use cot_macros::FromRequestHead; +/// use serde::Deserialize; +/// +/// #[derive(Debug, FromRequestHead)] +/// pub struct BaseContext { +/// urls: Urls, +/// static_files: StaticFiles, +/// } +/// ``` +pub use cot_macros::FromRequestHead; + +use crate::error::impl_into_cot_error; + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + use crate::html::Html; + use crate::request::extractors::{FromRequest, Json, Path, UrlQuery}; + + #[cfg(feature = "json")] + #[cot::test] + async fn json() { + let request = http::Request::builder() + .method(http::Method::POST) + .header( + http::header::CONTENT_TYPE, + crate::headers::JSON_CONTENT_TYPE, + ) + .body(Body::fixed(r#"{"hello":"world"}"#)) + .unwrap(); + + let (head, body) = request.into_parts(); + let Json(data): Json = Json::from_request(&head, body).await.unwrap(); + assert_eq!(data, serde_json::json!({"hello": "world"})); + } + + #[cfg(feature = "json")] + #[cot::test] + async fn json_empty() { + #[derive(Debug, Deserialize, PartialEq, Eq)] + struct TestData {} + + let request = http::Request::builder() + .method(http::Method::POST) + .header( + http::header::CONTENT_TYPE, + crate::headers::JSON_CONTENT_TYPE, + ) + .body(Body::fixed("{}")) + .unwrap(); + + let (head, body) = request.into_parts(); + let Json(data): Json = Json::from_request(&head, body).await.unwrap(); + assert_eq!(data, TestData {}); + } + + #[cfg(feature = "json")] + #[cot::test] + async fn json_struct() { + #[derive(Debug, Deserialize, PartialEq, Eq)] + struct TestDataInner { + hello: String, + } + + #[derive(Debug, Deserialize, PartialEq, Eq)] + struct TestData { + inner: TestDataInner, + } + + let request = http::Request::builder() + .method(http::Method::POST) + .header( + http::header::CONTENT_TYPE, + crate::headers::JSON_CONTENT_TYPE, + ) + .body(Body::fixed(r#"{"inner":{"hello":"world"}}"#)) + .unwrap(); + + let (head, body) = request.into_parts(); + let Json(data): Json = Json::from_request(&head, body).await.unwrap(); + assert_eq!( + data, + TestData { + inner: TestDataInner { + hello: "world".to_string(), + } + } + ); + } + + #[cot::test] + async fn path_extraction() { + #[derive(Deserialize, Debug, PartialEq)] + struct TestParams { + id: i32, + name: String, + } + + let (mut head, _body) = Request::new(Body::empty()).into_parts(); + + let mut params = PathParams::new(); + params.insert("id".to_string(), "42".to_string()); + params.insert("name".to_string(), "test".to_string()); + head.extensions.insert(params); + + let Path(extracted): Path = Path::from_request_head(&head).await.unwrap(); + let expected = TestParams { + id: 42, + name: "test".to_string(), + }; + + assert_eq!(extracted, expected); + } + + #[cot::test] + async fn url_query_extraction() { + #[derive(Deserialize, Debug, PartialEq)] + struct QueryParams { + page: i32, + filter: String, + } + + let (mut head, _body) = Request::new(Body::empty()).into_parts(); + head.uri = "https://example.com/?page=2&filter=active".parse().unwrap(); + + let UrlQuery(query): UrlQuery = + UrlQuery::from_request_head(&head).await.unwrap(); + + assert_eq!(query.page, 2); + assert_eq!(query.filter, "active"); + } + + #[cot::test] + async fn url_query_empty() { + #[derive(Deserialize, Debug, PartialEq)] + struct EmptyParams {} + + let (mut head, _body) = Request::new(Body::empty()).into_parts(); + head.uri = "https://example.com/".parse().unwrap(); + + let result: UrlQuery = UrlQuery::from_request_head(&head).await.unwrap(); + assert!(matches!(result, UrlQuery(_))); + } + + #[cfg(feature = "json")] + #[cot::test] + async fn json_invalid_content_type() { + let request = http::Request::builder() + .method(http::Method::POST) + .header(http::header::CONTENT_TYPE, "text/plain") + .body(Body::fixed(r#"{"hello":"world"}"#)) + .unwrap(); + + let (head, body) = request.into_parts(); + let result = Json::::from_request(&head, body).await; + assert!(result.is_err()); + } +} diff --git a/cot/src/request/path_params_deserializer.rs b/cot-core/src/request/path_params_deserializer.rs similarity index 100% rename from cot/src/request/path_params_deserializer.rs rename to cot-core/src/request/path_params_deserializer.rs diff --git a/cot/src/handler.rs b/cot/src/handler.rs index be8f6423c..1a508a482 100644 --- a/cot/src/handler.rs +++ b/cot/src/handler.rs @@ -72,6 +72,12 @@ pub(crate) fn into_box_request_handler + Send + Sync>( Inner(handler, PhantomData) } +/// Private marker type used to distinguish `FromRequest` parameter position in +/// trait impls. This is used instead of `()` to avoid coherence conflicts. +#[doc(hidden)] +#[expect(missing_copy_implementations, missing_debug_implementations)] +pub struct FromRequestMarker(()); + macro_rules! impl_request_handler { ($($ty:ident),*) => { impl RequestHandler<($($ty,)*)> for Func @@ -106,7 +112,7 @@ macro_rules! impl_request_handler { macro_rules! impl_request_handler_from_request { ($($ty_lhs:ident,)* ($ty_from_request:ident) $(,$ty_rhs:ident)*) => { - impl RequestHandler<($($ty_lhs,)* $ty_from_request, (), $($ty_rhs,)*)> for Func + impl RequestHandler<($($ty_lhs,)* $ty_from_request, FromRequestMarker, $($ty_rhs,)*)> for Func where Func: FnOnce($($ty_lhs,)* $ty_from_request, $($ty_rhs),*) -> Fut + Clone + Send + Sync + 'static, $($ty_lhs: FromRequestHead + Send,)* diff --git a/cot/src/request.rs b/cot/src/request.rs index 3122b8124..d89f2e49e 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -15,25 +15,19 @@ use std::future::Future; use std::sync::Arc; +#[doc(inline)] +pub(crate) use cot_core::request::{AppName, InvalidContentType, RouteName}; +#[doc(inline)] +pub use cot_core::request::{PathParams, PathParamsDeserializerError, Request, RequestHead}; use http::Extensions; -use indexmap::IndexMap; #[cfg(feature = "db")] use crate::db::Database; -use crate::error::impl_into_cot_error; use crate::request::extractors::FromRequestHead; use crate::router::Router; use crate::{Body, Result}; pub mod extractors; -mod path_params_deserializer; - -/// HTTP request type. -pub type Request = http::Request; - -/// HTTP request head type. -pub type RequestHead = http::request::Parts; - mod private { pub trait Sealed {} } @@ -391,269 +385,6 @@ impl RequestExt for RequestHead { } } -#[derive(Debug, thiserror::Error)] -#[error("invalid content type; expected `{expected}`, found `{actual}`")] -pub(crate) struct InvalidContentType { - expected: &'static str, - actual: String, -} -impl_into_cot_error!(InvalidContentType, BAD_REQUEST); - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct AppName(pub(crate) String); - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct RouteName(pub(crate) String); - -/// Path parameters extracted from the request URL, and available as a map of -/// strings. -/// -/// This struct is meant to be mainly used using the [`PathParams::parse`] -/// method, which will deserialize the path parameters into a type `T` -/// implementing `serde::DeserializeOwned`. If needed, you can also access the -/// path parameters directly using the [`PathParams::get`] method. -/// -/// # Examples -/// -/// ``` -/// use cot::request::{PathParams, Request, RequestExt}; -/// use cot::response::Response; -/// use cot::test::TestRequestBuilder; -/// -/// async fn my_handler(mut request: Request) -> cot::Result { -/// let path_params = request.path_params(); -/// let name = path_params.get("name").unwrap(); -/// -/// // using more ergonomic syntax: -/// let name: String = request.path_params().parse()?; -/// -/// let name = println!("Hello, {}!", name); -/// // ... -/// # unimplemented!() -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct PathParams { - params: IndexMap, -} - -impl Default for PathParams { - fn default() -> Self { - Self::new() - } -} - -impl PathParams { - /// Creates a new [`PathParams`] instance. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - #[must_use] - pub fn new() -> Self { - Self { - params: IndexMap::new(), - } - } - - /// Inserts a new path parameter. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - pub fn insert(&mut self, name: String, value: String) { - self.params.insert(name, value); - } - - /// Iterates over the path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// for (name, value) in path_params.iter() { - /// println!("{}: {}", name, value); - /// } - /// ``` - pub fn iter(&self) -> impl Iterator { - self.params - .iter() - .map(|(name, value)| (name.as_str(), value.as_str())) - } - - /// Returns the number of path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let path_params = PathParams::new(); - /// assert_eq!(path_params.len(), 0); - /// ``` - #[must_use] - pub fn len(&self) -> usize { - self.params.len() - } - - /// Returns `true` if the path parameters are empty. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let path_params = PathParams::new(); - /// assert!(path_params.is_empty()); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.params.is_empty() - } - - /// Returns the value of a path parameter. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - #[must_use] - pub fn get(&self, name: &str) -> Option<&str> { - self.params.get(name).map(String::as_str) - } - - /// Returns the value of a path parameter at the given index. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get_index(0), Some("world")); - /// ``` - #[must_use] - pub fn get_index(&self, index: usize) -> Option<&str> { - self.params - .get_index(index) - .map(|(_, value)| value.as_str()) - } - - /// Returns the key of a path parameter at the given index. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.key_at_index(0), Some("name")); - /// ``` - #[must_use] - pub fn key_at_index(&self, index: usize) -> Option<&str> { - self.params.get_index(index).map(|(key, _)| key.as_str()) - } - - /// Deserializes the path parameters into a type `T` implementing - /// `serde::DeserializeOwned`. - /// - /// # Errors - /// - /// Throws an error if the path parameters could not be deserialized. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// - /// let hello: String = path_params.parse()?; - /// assert_eq!(hello, "world"); - /// # Ok(()) - /// # } - /// ``` - /// - /// ``` - /// use cot::request::PathParams; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// path_params.insert("name".into(), "john".into()); - /// - /// let (hello, name): (String, String) = path_params.parse()?; - /// assert_eq!(hello, "world"); - /// assert_eq!(name, "john"); - /// # Ok(()) - /// # } - /// ``` - /// - /// ``` - /// use cot::request::PathParams; - /// use serde::Deserialize; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// path_params.insert("name".into(), "john".into()); - /// - /// #[derive(Deserialize)] - /// struct Params { - /// hello: String, - /// name: String, - /// } - /// - /// let params: Params = path_params.parse()?; - /// assert_eq!(params.hello, "world"); - /// assert_eq!(params.name, "john"); - /// # Ok(()) - /// # } - /// ``` - pub fn parse<'de, T: serde::Deserialize<'de>>( - &'de self, - ) -> std::result::Result { - let deserializer = path_params_deserializer::PathParamsDeserializer::new(self); - serde_path_to_error::deserialize(deserializer).map_err(PathParamsDeserializerError) - } -} - -/// An error that occurs when deserializing path parameters. -#[derive(Debug, Clone, thiserror::Error)] -#[error("could not parse path parameters: {0}")] -pub struct PathParamsDeserializerError( - // A wrapper over the original deserializer error. The exact error reason - // shouldn't be useful to the user, hence we're not exposing it. - #[source] serde_path_to_error::Error, -); -impl_into_cot_error!(PathParamsDeserializerError, BAD_REQUEST); - #[cfg(test)] mod tests { use super::*; @@ -662,37 +393,6 @@ mod tests { use crate::router::{Route, Router}; use crate::test::TestRequestBuilder; - #[test] - fn path_params() { - let mut path_params = PathParams::new(); - path_params.insert("name".into(), "world".into()); - - assert_eq!(path_params.get("name"), Some("world")); - assert_eq!(path_params.get("missing"), None); - } - - #[test] - fn path_params_parse() { - #[derive(Debug, PartialEq, Eq, serde::Deserialize)] - struct Params { - hello: String, - foo: String, - } - - let mut path_params = PathParams::new(); - path_params.insert("hello".into(), "world".into()); - path_params.insert("foo".into(), "bar".into()); - - let params: Params = path_params.parse().unwrap(); - assert_eq!( - params, - Params { - hello: "world".to_string(), - foo: "bar".to_string(), - } - ); - } - #[test] fn request_ext_app_name() { let mut request = TestRequestBuilder::get("/").build(); diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 59428b5f2..61214a2db 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -51,61 +51,17 @@ use std::future::Future; use std::sync::Arc; +#[doc(inline)] +pub use cot_core::request::extractors::*; use serde::de::DeserializeOwned; +use crate::Body; use crate::auth::Auth; +use crate::error::impl_into_cot_error; use crate::form::{Form, FormResult}; -#[cfg(feature = "json")] -use crate::json::Json; -use crate::request::{InvalidContentType, PathParams, Request, RequestExt, RequestHead}; +use crate::request::{Request, RequestExt, RequestHead}; use crate::router::Urls; use crate::session::Session; -use crate::{Body, Method}; - -/// Trait for extractors that consume the request body. -/// -/// Extractors implementing this trait are used in route handlers that consume -/// the request body and therefore can only be used once per request. -/// -/// See [`crate::request::extractors`] documentation for more information about -/// extractors. -pub trait FromRequest: Sized { - /// Extracts data from the request. - /// - /// # Errors - /// - /// Throws an error if the extractor fails to extract the data from the - /// request. - fn from_request( - head: &RequestHead, - body: Body, - ) -> impl Future> + Send; -} - -impl FromRequest for Request { - async fn from_request(head: &RequestHead, body: Body) -> cot::Result { - Ok(Request::from_parts(head.clone(), body)) - } -} - -/// Trait for extractors that don't consume the request body. -/// -/// Extractors implementing this trait are used in route handlers that don't -/// consume the request and therefore can be used multiple times per request. -/// -/// If you need to consume the body of the request, use [`FromRequest`] instead. -/// -/// See [`crate::request::extractors`] documentation for more information about -/// extractors. -pub trait FromRequestHead: Sized { - /// Extracts data from the request head. - /// - /// # Errors - /// - /// Throws an error if the extractor fails to extract the data from the - /// request head. - fn from_request_head(head: &RequestHead) -> impl Future> + Send; -} impl FromRequestHead for Urls { async fn from_request_head(head: &RequestHead) -> cot::Result { @@ -113,208 +69,6 @@ impl FromRequestHead for Urls { } } -/// An extractor that extracts data from the URL params. -/// -/// The extractor is generic over a type that implements -/// `serde::de::DeserializeOwned`. -/// -/// # Examples -/// -/// ``` -/// use cot::html::Html; -/// use cot::request::extractors::{FromRequest, Path}; -/// use cot::request::{Request, RequestExt}; -/// use cot::router::{Route, Router}; -/// use cot::test::TestRequestBuilder; -/// -/// async fn my_handler(Path(my_param): Path) -> Html { -/// Html::new(format!("Hello {my_param}!")) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let router = Router::with_urls([Route::with_handler_and_name( -/// "/{my_param}/", -/// my_handler, -/// "home", -/// )]); -/// let request = TestRequestBuilder::get("/world/") -/// .router(router.clone()) -/// .build(); -/// -/// assert_eq!( -/// router -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "Hello world!" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Path(pub D); - -impl FromRequestHead for Path { - async fn from_request_head(head: &RequestHead) -> cot::Result { - let params = head - .extensions - .get::() - .expect("PathParams extension missing") - .parse()?; - Ok(Self(params)) - } -} - -/// An extractor that extracts data from the URL query parameters. -/// -/// The extractor is generic over a type that implements -/// `serde::de::DeserializeOwned`. -/// -/// # Example -/// -/// ``` -/// use cot::RequestHandler; -/// use cot::html::Html; -/// use cot::request::extractors::{FromRequest, UrlQuery}; -/// use cot::router::{Route, Router}; -/// use cot::test::TestRequestBuilder; -/// use serde::Deserialize; -/// -/// #[derive(Deserialize)] -/// struct MyQuery { -/// hello: String, -/// } -/// -/// async fn my_handler(UrlQuery(query): UrlQuery) -> Html { -/// Html::new(format!("Hello {}!", query.hello)) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let request = TestRequestBuilder::get("/?hello=world").build(); -/// -/// assert_eq!( -/// my_handler -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "Hello world!" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Clone, Copy, Default)] -pub struct UrlQuery(pub T); - -impl FromRequestHead for UrlQuery -where - D: DeserializeOwned, -{ - async fn from_request_head(head: &RequestHead) -> cot::Result { - let query = head.uri.query().unwrap_or_default(); - - let deserializer = - serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); - - let value = - serde_path_to_error::deserialize(deserializer).map_err(QueryParametersParseError)?; - - Ok(UrlQuery(value)) - } -} - -#[derive(Debug, thiserror::Error)] -#[error("could not parse query parameters: {0}")] -struct QueryParametersParseError(serde_path_to_error::Error); -impl_into_cot_error!(QueryParametersParseError, BAD_REQUEST); - -/// Extractor that gets the request body as JSON and deserializes it into a type -/// `T` implementing `serde::de::DeserializeOwned`. -/// -/// The content type of the request must be `application/json`. -/// -/// # Errors -/// -/// Throws an error if the content type is not `application/json`. -/// Throws an error if the request body could not be read. -/// Throws an error if the request body could not be deserialized - either -/// because the JSON is invalid or because the deserialization to the target -/// structure failed. -/// -/// # Example -/// -/// ``` -/// use cot::RequestHandler; -/// use cot::json::Json; -/// use cot::test::TestRequestBuilder; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Serialize, Deserialize)] -/// struct MyData { -/// hello: String, -/// } -/// -/// async fn my_handler(Json(data): Json) -> Json { -/// Json(data) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let request = TestRequestBuilder::get("/") -/// .json(&MyData { -/// hello: "world".to_string(), -/// }) -/// .build(); -/// -/// assert_eq!( -/// my_handler -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "{\"hello\":\"world\"}" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[cfg(feature = "json")] -impl FromRequest for Json { - async fn from_request(head: &RequestHead, body: Body) -> cot::Result { - let content_type = head - .headers - .get(http::header::CONTENT_TYPE) - .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); - if content_type != cot::headers::JSON_CONTENT_TYPE { - return Err(InvalidContentType { - expected: cot::headers::JSON_CONTENT_TYPE, - actual: content_type.into_owned(), - } - .into()); - } - - let bytes = body.into_bytes().await?; - - let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); - let result = - serde_path_to_error::deserialize(deserializer).map_err(JsonDeserializeError)?; - - Ok(Self(result)) - } -} - -#[cfg(feature = "json")] -#[derive(Debug, thiserror::Error)] -#[error("JSON deserialization error: {0}")] -struct JsonDeserializeError(serde_path_to_error::Error); -#[cfg(feature = "json")] -impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); - /// An extractor that gets the request body as form data and deserializes it /// into a type `F` implementing `cot::form::Form`. /// @@ -487,19 +241,6 @@ impl FromRequestHead for StaticFiles { } } -// extractor impls for existing types -impl FromRequestHead for RequestHead { - async fn from_request_head(head: &RequestHead) -> cot::Result { - Ok(head.clone()) - } -} - -impl FromRequestHead for Method { - async fn from_request_head(head: &RequestHead) -> cot::Result { - Ok(head.method.clone()) - } -} - impl FromRequestHead for Session { async fn from_request_head(head: &RequestHead) -> cot::Result { Ok(Session::from_extensions(&head.extensions).clone()) @@ -518,184 +259,44 @@ impl FromRequestHead for Auth { } } -/// A derive macro that automatically implements the [`FromRequestHead`] trait -/// for structs. -/// -/// This macro generates code to extract each field of the struct from HTTP -/// request head, making it easy to create composite extractors that combine -/// multiple data sources from an incoming request. -/// -/// The macro works by calling [`FromRequestHead::from_request_head`] on each -/// field's type, allowing you to compose extractors seamlessly. All fields must -/// implement the [`FromRequestHead`] trait for the derivation to work. -/// -/// # Requirements -/// -/// - The target struct must have all fields implement [`FromRequestHead`] -/// - Works with named fields, unnamed fields (tuple structs), and unit structs -/// - The struct must be accessible where the macro is used -/// -/// # Examples -/// -/// ## Named Fields -/// -/// ```no_run -/// use cot::request::extractors::{Path, StaticFiles, UrlQuery}; -/// use cot::router::Urls; -/// use cot_macros::FromRequestHead; -/// use serde::Deserialize; -/// -/// #[derive(Debug, FromRequestHead)] -/// pub struct BaseContext { -/// urls: Urls, -/// static_files: StaticFiles, -/// } -/// ``` -pub use cot_macros::FromRequestHead; - -use crate::error::impl_into_cot_error; - #[cfg(test)] mod tests { - use serde::Deserialize; + use cot_core::Method; + use cot_core::html::Html; use super::*; - use crate::html::Html; - use crate::request::extractors::{FromRequest, Json, Path, UrlQuery}; - use crate::router::{Route, Router, Urls}; + use crate::request::extractors::FromRequest; + use crate::reverse; + use crate::router::{Route, Router}; use crate::test::TestRequestBuilder; - use crate::{Body, reverse}; - - #[cfg(feature = "json")] - #[cot::test] - async fn json() { - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) - .body(Body::fixed(r#"{"hello":"world"}"#)) - .unwrap(); - - let (head, body) = request.into_parts(); - let Json(data): Json = Json::from_request(&head, body).await.unwrap(); - assert_eq!(data, serde_json::json!({"hello": "world"})); - } - #[cfg(feature = "json")] #[cot::test] - async fn json_empty() { - #[derive(Debug, Deserialize, PartialEq, Eq)] - struct TestData {} - - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) - .body(Body::fixed("{}")) - .unwrap(); - - let (head, body) = request.into_parts(); - let Json(data): Json = Json::from_request(&head, body).await.unwrap(); - assert_eq!(data, TestData {}); - } - - #[cfg(feature = "json")] - #[cot::test] - async fn json_struct() { - #[derive(Debug, Deserialize, PartialEq, Eq)] - struct TestDataInner { - hello: String, - } - - #[derive(Debug, Deserialize, PartialEq, Eq)] - struct TestData { - inner: TestDataInner, - } - - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) - .body(Body::fixed(r#"{"inner":{"hello":"world"}}"#)) - .unwrap(); - - let (head, body) = request.into_parts(); - let Json(data): Json = Json::from_request(&head, body).await.unwrap(); - assert_eq!( - data, - TestData { - inner: TestDataInner { - hello: "world".to_string(), - } - } - ); - } - - #[cot::test] - async fn path_extraction() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestParams { - id: i32, - name: String, + async fn urls_extraction() { + async fn handler() -> Html { + Html::new("") } - let (mut head, _body) = Request::new(Body::empty()).into_parts(); - - let mut params = PathParams::new(); - params.insert("id".to_string(), "42".to_string()); - params.insert("name".to_string(), "test".to_string()); - head.extensions.insert(params); - - let Path(extracted): Path = Path::from_request_head(&head).await.unwrap(); - let expected = TestParams { - id: 42, - name: "test".to_string(), - }; - - assert_eq!(extracted, expected); - } - - #[cot::test] - async fn url_query_extraction() { - #[derive(Deserialize, Debug, PartialEq)] - struct QueryParams { - page: i32, - filter: String, - } + let router = Router::with_urls([Route::with_handler_and_name( + "/test/", + handler, + "test_route", + )]); - let (mut head, _body) = Request::new(Body::empty()).into_parts(); - head.uri = "https://example.com/?page=2&filter=active".parse().unwrap(); + let mut request = TestRequestBuilder::get("/test/").router(router).build(); - let UrlQuery(query): UrlQuery = - UrlQuery::from_request_head(&head).await.unwrap(); + let urls: Urls = request.extract_from_head().await.unwrap(); - assert_eq!(query.page, 2); - assert_eq!(query.filter, "active"); + assert!(reverse!(urls, "test_route").is_ok()); } #[cot::test] - async fn url_query_empty() { - #[derive(Deserialize, Debug, PartialEq)] - struct EmptyParams {} - - let (mut head, _body) = Request::new(Body::empty()).into_parts(); - head.uri = "https://example.com/".parse().unwrap(); - - let result: UrlQuery = UrlQuery::from_request_head(&head).await.unwrap(); - assert!(matches!(result, UrlQuery(_))); - } + async fn method_extraction() { + let mut request = TestRequestBuilder::get("/test/").build(); - #[cfg(feature = "json")] - #[cot::test] - async fn json_invalid_content_type() { - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, "text/plain") - .body(Body::fixed(r#"{"hello":"world"}"#)) - .unwrap(); + let method: Method = request.extract_from_head().await.unwrap(); - let (head, body) = request.into_parts(); - let result = Json::::from_request(&head, body).await; - assert!(result.is_err()); + assert_eq!(method, Method::GET); } - #[cot::test] async fn request_form() { #[derive(Debug, PartialEq, Eq, Form)] @@ -721,34 +322,6 @@ mod tests { ); } - #[cot::test] - async fn urls_extraction() { - async fn handler() -> Html { - Html::new("") - } - - let router = Router::with_urls([Route::with_handler_and_name( - "/test/", - handler, - "test_route", - )]); - - let mut request = TestRequestBuilder::get("/test/").router(router).build(); - - let urls: Urls = request.extract_from_head().await.unwrap(); - - assert!(reverse!(urls, "test_route").is_ok()); - } - - #[cot::test] - async fn method_extraction() { - let mut request = TestRequestBuilder::get("/test/").build(); - - let method: Method = request.extract_from_head().await.unwrap(); - - assert_eq!(method, Method::GET); - } - #[cfg(feature = "db")] #[cot::test] #[cfg_attr( From 266214e531191e4e26d77ef9d00fde7fa06f201d Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 20:15:24 +0100 Subject: [PATCH 10/14] move middleware --- Cargo.lock | 2 + cot-core/Cargo.toml | 2 + cot-core/src/lib.rs | 1 + cot-core/src/middleware.rs | 238 +++++++++++++++++++++++++++++++++++++ cot/src/middleware.rs | 232 +----------------------------------- 5 files changed, 247 insertions(+), 228 deletions(-) create mode 100644 cot-core/src/middleware.rs diff --git a/Cargo.lock b/Cargo.lock index 289d1857d..58cfc135b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,6 +805,7 @@ dependencies = [ "form_urlencoded", "futures", "futures-core", + "futures-util", "http 1.4.0", "http-body", "http-body-util", @@ -816,6 +817,7 @@ dependencies = [ "sync_wrapper", "thiserror 2.0.17", "tokio", + "tower", "tower-sessions", ] diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 96a4bf6a1..17f706e25 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -35,6 +35,8 @@ serde_path_to_error.workspace = true indexmap.workspace = true serde_html_form.workspace = true form_urlencoded.workspace = true +tower.workspace = true +futures-util.workspace = true [dev-dependencies] async-stream.workspace = true diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index fa01c3b80..d69726d9f 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -5,6 +5,7 @@ pub mod headers; pub mod html; #[cfg(feature = "json")] pub mod json; +pub mod middleware; pub mod request; pub mod response; diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs new file mode 100644 index 000000000..d79b57049 --- /dev/null +++ b/cot-core/src/middleware.rs @@ -0,0 +1,238 @@ +use std::fmt::Debug; +use std::task::{Context, Poll}; + +use bytes::Bytes; +use futures_util::TryFutureExt; +use http_body_util::BodyExt; +use http_body_util::combinators::BoxBody; +use tower::Service; + +use crate::request::Request; +use crate::response::Response; +use crate::{Body, Error}; + +/// Middleware that converts a any [`http::Response`] generic type to a +/// [`cot::response::Response`]. +/// +/// This is useful for converting a response from a middleware that is +/// compatible with the `tower` crate to a response that is compatible with +/// Cot. It's applied automatically by +/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) +/// and is not needed to be added manually. +/// +/// # Examples +/// +/// ``` +/// use cot::Project; +/// use cot::middleware::LiveReloadMiddleware; +/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; +/// +/// struct MyProject; +/// impl Project for MyProject { +/// fn middlewares( +/// &self, +/// handler: RootHandlerBuilder, +/// context: &MiddlewareContext, +/// ) -> RootHandler { +/// handler +/// // IntoCotResponseLayer used internally in middleware() +/// .middleware(LiveReloadMiddleware::from_context(context)) +/// .build() +/// } +/// } +/// ``` +#[derive(Debug, Copy, Clone)] +pub struct IntoCotResponseLayer; + +impl IntoCotResponseLayer { + /// Create a new [`IntoCotResponseLayer`]. + /// + /// # Examples + /// + /// ``` + /// use cot::middleware::IntoCotResponseLayer; + /// + /// let middleware = IntoCotResponseLayer::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self {} + } +} + +impl Default for IntoCotResponseLayer { + fn default() -> Self { + Self::new() + } +} + +impl tower::Layer for IntoCotResponseLayer { + type Service = IntoCotResponse; + + fn layer(&self, inner: S) -> Self::Service { + IntoCotResponse { inner } + } +} + +/// Service struct that converts any [`http::Response`] generic type to +/// [`cot::response::Response`]. +/// +/// Used by [`IntoCotResponseLayer`]. +/// +/// # Examples +/// +/// ``` +/// use std::any::TypeId; +/// +/// use cot::middleware::{IntoCotResponse, IntoCotResponseLayer}; +/// +/// assert_eq!( +/// TypeId::of::<>::Service>(), +/// TypeId::of::>() +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct IntoCotResponse { + inner: S, +} + +impl Service for IntoCotResponse +where + S: Service>, + ResBody: http_body::Body + Send + Sync + 'static, + E: std::error::Error + Send + Sync + 'static, +{ + type Response = Response; + type Error = S::Error; + type Future = futures_util::future::MapOk) -> Response>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + #[inline] + fn call(&mut self, request: Request) -> Self::Future { + self.inner.call(request).map_ok(map_response) + } +} + +fn map_response(response: http::response::Response) -> Response +where + ResBody: http_body::Body + Send + Sync + 'static, + E: std::error::Error + Send + Sync + 'static, +{ + response.map(|body| Body::wrapper(BoxBody::new(body.map_err(map_err)))) +} + +/// Middleware that converts any error type to [`cot::Error`]. +/// +/// This is useful for converting a response from a middleware that is +/// compatible with the `tower` crate to a response that is compatible with +/// Cot. It's applied automatically by +/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) +/// and is not needed to be added manually. +/// +/// # Examples +/// +/// ``` +/// use cot::Project; +/// use cot::middleware::LiveReloadMiddleware; +/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; +/// +/// struct MyProject; +/// impl Project for MyProject { +/// fn middlewares( +/// &self, +/// handler: RootHandlerBuilder, +/// context: &MiddlewareContext, +/// ) -> RootHandler { +/// handler +/// // IntoCotErrorLayer used internally in middleware() +/// .middleware(LiveReloadMiddleware::from_context(context)) +/// .build() +/// } +/// } +/// ``` +#[derive(Debug, Copy, Clone)] +pub struct IntoCotErrorLayer; + +impl IntoCotErrorLayer { + /// Create a new [`IntoCotErrorLayer`]. + /// + /// # Examples + /// + /// ``` + /// use cot::middleware::IntoCotErrorLayer; + /// + /// let middleware = IntoCotErrorLayer::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self {} + } +} + +impl Default for IntoCotErrorLayer { + fn default() -> Self { + Self::new() + } +} + +impl tower::Layer for IntoCotErrorLayer { + type Service = IntoCotError; + + fn layer(&self, inner: S) -> Self::Service { + IntoCotError { inner } + } +} + +/// Service struct that converts a any error type to a [`cot::Error`]. +/// +/// Used by [`IntoCotErrorLayer`]. +/// +/// # Examples +/// +/// ``` +/// use std::any::TypeId; +/// +/// use cot::middleware::{IntoCotError, IntoCotErrorLayer}; +/// +/// assert_eq!( +/// TypeId::of::<>::Service>(), +/// TypeId::of::>() +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct IntoCotError { + inner: S, +} + +impl Service for IntoCotError +where + S: Service, + >::Error: std::error::Error + Send + Sync + 'static, +{ + type Response = S::Response; + type Error = Error; + type Future = futures_util::future::MapErr Error>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(map_err) + } + + #[inline] + fn call(&mut self, request: Request) -> Self::Future { + self.inner.call(request).map_err(map_err) + } +} + +fn map_err(error: E) -> Error +where + E: std::error::Error + Send + Sync + 'static, +{ + #[expect(trivial_casts)] + let boxed = Box::new(error) as Box; + boxed.downcast::().map_or_else(Error::wrap, |e| *e) +} diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index b9e6e27cc..077503fbf 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -9,11 +9,9 @@ use std::fmt::Debug; use std::sync::Arc; use std::task::{Context, Poll}; -use bytes::Bytes; use futures_core::future::BoxFuture; use futures_util::TryFutureExt; use http_body_util::BodyExt; -use http_body_util::combinators::BoxBody; use tower::Service; use tower_sessions::service::PlaintextCookie; use tower_sessions::{SessionManagerLayer, SessionStore}; @@ -37,235 +35,13 @@ use crate::{Body, Error}; #[cfg(feature = "live-reload")] mod live_reload; +#[doc(inline)] +pub use cot_core::middleware::{ + IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer, +}; #[cfg(feature = "live-reload")] pub use live_reload::LiveReloadMiddleware; -/// Middleware that converts a any [`http::Response`] generic type to a -/// [`cot::response::Response`]. -/// -/// This is useful for converting a response from a middleware that is -/// compatible with the `tower` crate to a response that is compatible with -/// Cot. It's applied automatically by -/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) -/// and is not needed to be added manually. -/// -/// # Examples -/// -/// ``` -/// use cot::Project; -/// use cot::middleware::LiveReloadMiddleware; -/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; -/// -/// struct MyProject; -/// impl Project for MyProject { -/// fn middlewares( -/// &self, -/// handler: RootHandlerBuilder, -/// context: &MiddlewareContext, -/// ) -> RootHandler { -/// handler -/// // IntoCotResponseLayer used internally in middleware() -/// .middleware(LiveReloadMiddleware::from_context(context)) -/// .build() -/// } -/// } -/// ``` -#[derive(Debug, Copy, Clone)] -pub struct IntoCotResponseLayer; - -impl IntoCotResponseLayer { - /// Create a new [`IntoCotResponseLayer`]. - /// - /// # Examples - /// - /// ``` - /// use cot::middleware::IntoCotResponseLayer; - /// - /// let middleware = IntoCotResponseLayer::new(); - /// ``` - #[must_use] - pub fn new() -> Self { - Self {} - } -} - -impl Default for IntoCotResponseLayer { - fn default() -> Self { - Self::new() - } -} - -impl tower::Layer for IntoCotResponseLayer { - type Service = IntoCotResponse; - - fn layer(&self, inner: S) -> Self::Service { - IntoCotResponse { inner } - } -} - -/// Service struct that converts any [`http::Response`] generic type to -/// [`cot::response::Response`]. -/// -/// Used by [`IntoCotResponseLayer`]. -/// -/// # Examples -/// -/// ``` -/// use std::any::TypeId; -/// -/// use cot::middleware::{IntoCotResponse, IntoCotResponseLayer}; -/// -/// assert_eq!( -/// TypeId::of::<>::Service>(), -/// TypeId::of::>() -/// ); -/// ``` -#[derive(Debug, Clone)] -pub struct IntoCotResponse { - inner: S, -} - -impl Service for IntoCotResponse -where - S: Service>, - ResBody: http_body::Body + Send + Sync + 'static, - E: std::error::Error + Send + Sync + 'static, -{ - type Response = Response; - type Error = S::Error; - type Future = futures_util::future::MapOk) -> Response>; - - #[inline] - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - #[inline] - fn call(&mut self, request: Request) -> Self::Future { - self.inner.call(request).map_ok(map_response) - } -} - -fn map_response(response: http::response::Response) -> Response -where - ResBody: http_body::Body + Send + Sync + 'static, - E: std::error::Error + Send + Sync + 'static, -{ - response.map(|body| Body::wrapper(BoxBody::new(body.map_err(map_err)))) -} - -/// Middleware that converts any error type to [`cot::Error`]. -/// -/// This is useful for converting a response from a middleware that is -/// compatible with the `tower` crate to a response that is compatible with -/// Cot. It's applied automatically by -/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) -/// and is not needed to be added manually. -/// -/// # Examples -/// -/// ``` -/// use cot::Project; -/// use cot::middleware::LiveReloadMiddleware; -/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; -/// -/// struct MyProject; -/// impl Project for MyProject { -/// fn middlewares( -/// &self, -/// handler: RootHandlerBuilder, -/// context: &MiddlewareContext, -/// ) -> RootHandler { -/// handler -/// // IntoCotErrorLayer used internally in middleware() -/// .middleware(LiveReloadMiddleware::from_context(context)) -/// .build() -/// } -/// } -/// ``` -#[derive(Debug, Copy, Clone)] -pub struct IntoCotErrorLayer; - -impl IntoCotErrorLayer { - /// Create a new [`IntoCotErrorLayer`]. - /// - /// # Examples - /// - /// ``` - /// use cot::middleware::IntoCotErrorLayer; - /// - /// let middleware = IntoCotErrorLayer::new(); - /// ``` - #[must_use] - pub fn new() -> Self { - Self {} - } -} - -impl Default for IntoCotErrorLayer { - fn default() -> Self { - Self::new() - } -} - -impl tower::Layer for IntoCotErrorLayer { - type Service = IntoCotError; - - fn layer(&self, inner: S) -> Self::Service { - IntoCotError { inner } - } -} - -/// Service struct that converts a any error type to a [`cot::Error`]. -/// -/// Used by [`IntoCotErrorLayer`]. -/// -/// # Examples -/// -/// ``` -/// use std::any::TypeId; -/// -/// use cot::middleware::{IntoCotError, IntoCotErrorLayer}; -/// -/// assert_eq!( -/// TypeId::of::<>::Service>(), -/// TypeId::of::>() -/// ); -/// ``` -#[derive(Debug, Clone)] -pub struct IntoCotError { - inner: S, -} - -impl Service for IntoCotError -where - S: Service, - >::Error: std::error::Error + Send + Sync + 'static, -{ - type Response = S::Response; - type Error = Error; - type Future = futures_util::future::MapErr Error>; - - #[inline] - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(map_err) - } - - #[inline] - fn call(&mut self, request: Request) -> Self::Future { - self.inner.call(request).map_err(map_err) - } -} - -fn map_err(error: E) -> Error -where - E: std::error::Error + Send + Sync + 'static, -{ - #[expect(trivial_casts)] - let boxed = Box::new(error) as Box; - boxed.downcast::().map_or_else(Error::wrap, |e| *e) -} - type DynamicSessionStore = SessionManagerLayer; /// A middleware that provides session management. From c1c890a3309f180f512ac9760384d43b53a22398 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 11 Jan 2026 20:30:10 +0100 Subject: [PATCH 11/14] move handler --- {cot => cot-core}/src/handler.rs | 7 ++++--- cot-core/src/lib.rs | 2 ++ cot/src/lib.rs | 4 ++-- cot/src/openapi.rs | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) rename {cot => cot-core}/src/handler.rs (98%) diff --git a/cot/src/handler.rs b/cot-core/src/handler.rs similarity index 98% rename from cot/src/handler.rs rename to cot-core/src/handler.rs index 1a508a482..a3aa7c9d3 100644 --- a/cot/src/handler.rs +++ b/cot-core/src/handler.rs @@ -48,14 +48,14 @@ pub trait RequestHandler { fn handle(&self, request: Request) -> impl Future> + Send; } -pub(crate) trait BoxRequestHandler { +pub trait BoxRequestHandler { fn handle( &self, request: Request, ) -> Pin> + Send + '_>>; } -pub(crate) fn into_box_request_handler + Send + Sync>( +pub fn into_box_request_handler + Send + Sync>( handler: H, ) -> impl BoxRequestHandler { struct Inner(H, PhantomData T>); @@ -148,6 +148,7 @@ macro_rules! impl_request_handler_from_request { }; } +#[macro_export] macro_rules! handle_all_parameters { ($name:ident) => { $name!(); @@ -233,7 +234,7 @@ macro_rules! handle_all_parameters_from_request { }; } -pub(crate) use handle_all_parameters; +pub use handle_all_parameters; handle_all_parameters!(impl_request_handler); handle_all_parameters_from_request!(impl_request_handler_from_request); diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index d69726d9f..aff0c8721 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,6 +1,8 @@ mod body; pub mod error; +#[macro_use] +pub mod handler; pub mod headers; pub mod html; #[cfg(feature = "json")] diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 5c0d2863e..3efdd5300 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -72,8 +72,8 @@ pub mod cli; pub mod common_types; pub mod config; mod error_page; -#[macro_use] -pub(crate) mod handler; +#[doc(inline)] +pub(crate) use cot_core::handler; pub mod middleware; #[cfg(feature = "openapi")] pub mod openapi; diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 5be3cc055..d0958bd4b 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -207,7 +207,7 @@ use serde_json::Value; use crate::auth::Auth; use crate::form::Form; -use crate::handler::BoxRequestHandler; +use crate::handler::{BoxRequestHandler, handle_all_parameters}; use crate::json::Json; use crate::request::extractors::{FromRequest, FromRequestHead, Path, RequestForm, UrlQuery}; use crate::request::{Request, RequestHead}; From ead28ff679d2196e078404e8c2fba2843e369146 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Mon, 12 Jan 2026 18:59:23 +0100 Subject: [PATCH 12/14] fixes --- Cargo.lock | 7 ------- cot-core/Cargo.toml | 4 ++-- cot-core/src/error/backtrace.rs | 1 + cot-core/src/request.rs | 4 +--- cot-core/src/request/extractors.rs | 1 - cot/Cargo.toml | 7 ------- cot/src/error_page.rs | 1 - cot/src/middleware.rs | 4 +--- cot/src/request.rs | 3 ++- cot/src/request/extractors.rs | 2 -- 10 files changed, 7 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58cfc135b..0c750514a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,10 +678,8 @@ dependencies = [ "ahash", "aide", "askama", - "async-stream", "async-trait", "axum", - "backtrace", "bytes", "chrono", "chrono-tz", @@ -697,14 +695,12 @@ dependencies = [ "fake", "fantoccini", "form_urlencoded", - "futures", "futures-core", "futures-util", "grass", "hex", "hmac", "http 1.4.0", - "http-body", "http-body-util", "humantime", "indexmap", @@ -721,15 +717,12 @@ dependencies = [ "sea-query", "sea-query-binder", "serde", - "serde_html_form", "serde_json", - "serde_path_to_error", "serde_urlencoded", "sha2", "sqlx", "subtle", "swagger-ui-redist", - "sync_wrapper", "tempfile", "thiserror 2.0.17", "time", diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 17f706e25..ea60d217e 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -17,7 +17,7 @@ workspace = true [dependencies] http.workspace = true -derive_more.workspace = true +derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] } thiserror.workspace = true serde.workspace = true serde_json.workspace = true @@ -29,7 +29,7 @@ http-body-util.workspace = true sync_wrapper.workspace = true axum.workspace = true cot_macros.workspace = true -askama.workspace = true +askama = { workspace = true, features = ["alloc"] } tower-sessions.workspace = true serde_path_to_error.workspace = true indexmap.workspace = true diff --git a/cot-core/src/error/backtrace.rs b/cot-core/src/error/backtrace.rs index 9827b76e9..e06f1d818 100644 --- a/cot-core/src/error/backtrace.rs +++ b/cot-core/src/error/backtrace.rs @@ -1,6 +1,7 @@ // inline(never) is added to make sure there is a separate frame for this // function so that it can be used to find the start of the backtrace. #[inline(never)] +#[must_use] pub fn __cot_create_backtrace() -> Backtrace { let mut backtrace = Vec::new(); let mut start = false; diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs index ba91effcb..fddb2a74f 100644 --- a/cot-core/src/request.rs +++ b/cot-core/src/request.rs @@ -15,8 +15,6 @@ use indexmap::IndexMap; use crate::Body; -#[cfg(feature = "db")] -use crate::db::Database; use crate::error::impl_into_cot_error; pub mod extractors; @@ -275,7 +273,7 @@ impl PathParams { /// ``` pub fn parse<'de, T: serde::Deserialize<'de>>( &'de self, - ) -> std::result::Result { + ) -> Result { let deserializer = path_params_deserializer::PathParamsDeserializer::new(self); serde_path_to_error::deserialize(deserializer).map_err(PathParamsDeserializerError) } diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs index 8e6f0ace7..0bbb73fb0 100644 --- a/cot-core/src/request/extractors.rs +++ b/cot-core/src/request/extractors.rs @@ -359,7 +359,6 @@ mod tests { use serde::Deserialize; use super::*; - use crate::html::Html; use crate::request::extractors::{FromRequest, Json, Path, UrlQuery}; #[cfg(feature = "json")] diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 7894e66fe..21a06746f 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -20,7 +20,6 @@ aide = { workspace = true, optional = true } askama = { workspace = true, features = ["derive", "std"] } async-trait.workspace = true axum = { workspace = true, features = ["http1", "tokio"] } -backtrace.workspace = true bytes.workspace = true chrono = { workspace = true, features = ["alloc", "serde", "clock"] } chrono-tz.workspace = true @@ -39,7 +38,6 @@ futures-util.workspace = true hex.workspace = true hmac.workspace = true http-body-util.workspace = true -http-body.workspace = true http.workspace = true humantime.workspace = true indexmap.workspace = true @@ -53,14 +51,11 @@ schemars = { workspace = true, optional = true } sea-query = { workspace = true, optional = true } sea-query-binder = { workspace = true, features = ["with-chrono", "runtime-tokio"], optional = true } serde = { workspace = true, features = ["derive"] } -serde_html_form = { workspace = true } serde_json = { workspace = true, optional = true } -serde_path_to_error = { workspace = true } sha2.workspace = true sqlx = { workspace = true, features = ["runtime-tokio", "chrono"], optional = true } subtle = { workspace = true, features = ["std"] } swagger-ui-redist = { workspace = true, optional = true } -sync_wrapper.workspace = true thiserror.workspace = true time.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "fs", "io-util"] } @@ -72,11 +67,9 @@ tracing.workspace = true url = { workspace = true, features = ["serde"] } [dev-dependencies] -async-stream.workspace = true criterion = { workspace = true, features = ["async_tokio"] } fake.workspace = true fantoccini.workspace = true -futures.workspace = true mockall.workspace = true reqwest = { workspace = true, features = ["json"] } rustversion.workspace = true diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index b5437a6a6..1cd510680 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -73,7 +73,6 @@ impl ErrorPageTemplateBuilder { if let Some(not_found) = error.inner().downcast_ref::() { use crate::error::NotFoundKind as Kind; match ¬_found.kind { - Kind::FromRouter { .. } => {} Kind::Custom { .. } => { Self::build_error_data(&mut error_data, error); } diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index 077503fbf..eb91f7f3c 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -10,12 +10,11 @@ use std::sync::Arc; use std::task::{Context, Poll}; use futures_core::future::BoxFuture; -use futures_util::TryFutureExt; -use http_body_util::BodyExt; use tower::Service; use tower_sessions::service::PlaintextCookie; use tower_sessions::{SessionManagerLayer, SessionStore}; +use crate::Error; #[cfg(feature = "cache")] use crate::config::CacheType; use crate::config::{Expiry, SameSite, SessionStoreTypeConfig}; @@ -30,7 +29,6 @@ use crate::session::store::file::FileStore; use crate::session::store::memory::MemoryStore; #[cfg(feature = "redis")] use crate::session::store::redis::RedisStore; -use crate::{Body, Error}; #[cfg(feature = "live-reload")] mod live_reload; diff --git a/cot/src/request.rs b/cot/src/request.rs index d89f2e49e..c4bea871a 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -21,11 +21,11 @@ pub(crate) use cot_core::request::{AppName, InvalidContentType, RouteName}; pub use cot_core::request::{PathParams, PathParamsDeserializerError, Request, RequestHead}; use http::Extensions; +use crate::Result; #[cfg(feature = "db")] use crate::db::Database; use crate::request::extractors::FromRequestHead; use crate::router::Router; -use crate::{Body, Result}; pub mod extractors; mod private { @@ -388,6 +388,7 @@ impl RequestExt for RequestHead { #[cfg(test)] mod tests { use super::*; + use crate::Body; use crate::request::extractors::Path; use crate::response::Response; use crate::router::{Route, Router}; diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 61214a2db..c2c759629 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -48,12 +48,10 @@ //! # } //! ``` -use std::future::Future; use std::sync::Arc; #[doc(inline)] pub use cot_core::request::extractors::*; -use serde::de::DeserializeOwned; use crate::Body; use crate::auth::Auth; From d13c649d6b600957b0eaeb93772c03e8436c2693 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Thu, 15 Jan 2026 19:33:52 +0100 Subject: [PATCH 13/14] add body docs --- cot-core/src/body.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cot-core/src/body.rs b/cot-core/src/body.rs index e576d44e2..bd05b3351 100644 --- a/cot-core/src/body.rs +++ b/cot-core/src/body.rs @@ -1,3 +1,8 @@ +//! HTTP body type. +//! +//! This module provides the [`Body`] type for representing HTTP bodies, +//! supporting both fixed in-memory buffers and streaming data sources. + use std::error::Error as StdError; use std::fmt::{Debug, Formatter}; use std::pin::Pin; From 9b372af91c19a10935907f36d3a5ccd811f3766e Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Thu, 15 Jan 2026 19:42:15 +0100 Subject: [PATCH 14/14] remove no longer needed marker --- cot-core/src/error.rs | 6 ++++++ cot-core/src/handler.rs | 26 ++++++++++++++------------ cot-core/src/headers.rs | 4 ++++ cot-core/src/lib.rs | 10 ++++++++++ cot-core/src/middleware.rs | 24 ++++++++++++++++-------- cot-core/src/request.rs | 13 +++++++------ cot-core/src/response.rs | 5 +++-- cot/src/error.rs | 6 ++++++ cot/src/error/handler.rs | 2 +- cot/src/lib.rs | 5 ----- cot/src/session/store.rs | 9 +++++---- 11 files changed, 72 insertions(+), 38 deletions(-) diff --git a/cot-core/src/error.rs b/cot-core/src/error.rs index 592b87ea9..d73e50c24 100644 --- a/cot-core/src/error.rs +++ b/cot-core/src/error.rs @@ -1,3 +1,9 @@ +//! Error handling types and utilities for Cot applications. +//! +//! This module provides error types, error handlers, and utilities for +//! handling various types of errors that can occur in Cot applications, +//! including 404 Not Found errors, uncaught panics, and custom error pages. + pub mod backtrace; pub(crate) mod error_impl; mod method_not_allowed; diff --git a/cot-core/src/handler.rs b/cot-core/src/handler.rs index a3aa7c9d3..1b24a7162 100644 --- a/cot-core/src/handler.rs +++ b/cot-core/src/handler.rs @@ -1,3 +1,9 @@ +//! Request handler traits and utilities. +//! +//! This module provides the [`RequestHandler`] trait, which is the core +//! abstraction for handling HTTP requests in Cot. It is automatically +//! implemented for async functions taking extractors and returning responses. + use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; @@ -72,12 +78,6 @@ pub fn into_box_request_handler + Send + Sync>( Inner(handler, PhantomData) } -/// Private marker type used to distinguish `FromRequest` parameter position in -/// trait impls. This is used instead of `()` to avoid coherence conflicts. -#[doc(hidden)] -#[expect(missing_copy_implementations, missing_debug_implementations)] -pub struct FromRequestMarker(()); - macro_rules! impl_request_handler { ($($ty:ident),*) => { impl RequestHandler<($($ty,)*)> for Func @@ -112,7 +112,7 @@ macro_rules! impl_request_handler { macro_rules! impl_request_handler_from_request { ($($ty_lhs:ident,)* ($ty_from_request:ident) $(,$ty_rhs:ident)*) => { - impl RequestHandler<($($ty_lhs,)* $ty_from_request, FromRequestMarker, $($ty_rhs,)*)> for Func + impl RequestHandler<($($ty_lhs,)* $ty_from_request, (), $($ty_rhs,)*)> for Func where Func: FnOnce($($ty_lhs,)* $ty_from_request, $($ty_rhs),*) -> Fut + Clone + Send + Sync + 'static, $($ty_lhs: FromRequestHead + Send,)* @@ -239,14 +239,16 @@ pub use handle_all_parameters; handle_all_parameters!(impl_request_handler); handle_all_parameters_from_request!(impl_request_handler_from_request); +#[rustfmt::skip] // `wrap_comments` breaks local links /// A wrapper around a handler that's used in -/// [`Bootstrapper`](cot::Bootstrapper). +/// [`Bootstrapper`](../../cot/project/struct.Bootstrapper.html). /// /// It is returned by -/// [`Bootstrapper::into_bootstrapped_project`](cot::Bootstrapper::finish). -/// Typically, you don't need to interact with this type directly, except for -/// creating it in [`Project::middlewares`](cot::Project::middlewares) through -/// the [`RootHandlerBuilder::build`](cot::project::RootHandlerBuilder::build) +/// [`Bootstrapper::finish()`](../../cot/project/struct.Bootstrapper.html#method.finish). +/// Typically, you don't need to interact with this type directly, except +/// for creating it in +/// [`Project::middlewares()`](../../cot/project/trait.Project.html#method.middlewares) through the +/// [`RootHandlerBuilder::build()`](../../cot/project/struct.RootHandlerBuilder.html#method.build) /// method. /// /// # Examples diff --git a/cot-core/src/headers.rs b/cot-core/src/headers.rs index 746790b2e..52458b055 100644 --- a/cot-core/src/headers.rs +++ b/cot-core/src/headers.rs @@ -1,3 +1,7 @@ +//! HTTP header constants. +//! +//! This module provides commonly used content type header values. + pub const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8"; pub const MULTIPART_FORM_CONTENT_TYPE: &str = "multipart/form-data"; pub const URLENCODED_FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index aff0c8721..bd0e82093 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,3 +1,13 @@ +//! Core types and functionality for the Cot web framework. +//! +//! This crate provides the foundational building blocks for +//! [Cot](https://docs.rs/cot/latest/cot/), including HTTP primitives, body handling, error +//! types, handlers, middleware, and request/response types. +//! +//! Most applications should use the main `cot` crate rather than depending on +//! `cot-core` directly. This crate is primarily intended for internal use by +//! the Cot framework and for building custom extensions. + mod body; pub mod error; diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs index d79b57049..0a1bdbdc5 100644 --- a/cot-core/src/middleware.rs +++ b/cot-core/src/middleware.rs @@ -1,3 +1,9 @@ +//! Middlewares for modifying requests and responses. +//! +//! Middlewares are used to modify requests and responses in a pipeline. They +//! are used to add functionality to the request/response cycle, such as +//! session management, adding security headers, and more. + use std::fmt::Debug; use std::task::{Context, Poll}; @@ -11,13 +17,14 @@ use crate::request::Request; use crate::response::Response; use crate::{Body, Error}; -/// Middleware that converts a any [`http::Response`] generic type to a -/// [`cot::response::Response`]. +#[rustfmt::skip] // `wrap_comments` breaks local links +/// Middleware that converts any [`http::Response`] generic type +/// to a [`Response`]. /// /// This is useful for converting a response from a middleware that is /// compatible with the `tower` crate to a response that is compatible with /// Cot. It's applied automatically by -/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) +/// [`RootHandlerBuilder::middleware()`](../../cot/project/struct.RootHandlerBuilder.html#method.middleware) /// and is not needed to be added manually. /// /// # Examples @@ -74,8 +81,8 @@ impl tower::Layer for IntoCotResponseLayer { } } -/// Service struct that converts any [`http::Response`] generic type to -/// [`cot::response::Response`]. +/// Service struct that converts any [`http::Response`] generic +/// type to [`Response`]. /// /// Used by [`IntoCotResponseLayer`]. /// @@ -125,12 +132,13 @@ where response.map(|body| Body::wrapper(BoxBody::new(body.map_err(map_err)))) } -/// Middleware that converts any error type to [`cot::Error`]. +#[rustfmt::skip] // `wrap_comments` breaks local links +/// Middleware that converts any error type to [`Error`]. /// /// This is useful for converting a response from a middleware that is /// compatible with the `tower` crate to a response that is compatible with /// Cot. It's applied automatically by -/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) +/// [`RootHandlerBuilder::middleware`](../../cot/project/struct.RootHandlerBuilder.html#method.middleware) /// and is not needed to be added manually. /// /// # Examples @@ -187,7 +195,7 @@ impl tower::Layer for IntoCotErrorLayer { } } -/// Service struct that converts a any error type to a [`cot::Error`]. +/// Service struct that converts a any error type to a [`Error`]. /// /// Used by [`IntoCotErrorLayer`]. /// diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs index fddb2a74f..c6ada6fa9 100644 --- a/cot-core/src/request.rs +++ b/cot-core/src/request.rs @@ -2,11 +2,12 @@ //! //! Cot uses the [`Request`](http::Request) type from the [`http`] crate //! to represent incoming HTTP requests. However, it also provides a -//! [`RequestExt`] trait that contain various helper methods for working with -//! HTTP requests. These methods are used to access the application context, -//! project configuration, path parameters, and more. You probably want to have -//! a `use` statement for [`RequestExt`] in your code most of the time to be -//! able to use these functions: +//! [`RequestExt`](../../cot/request/trait.RequestExt.html) trait that contain +//! various helper methods for working with HTTP requests. These methods are +//! used to access the application context, project configuration, path +//! parameters, and more. You probably want to have a `use` statement for +//! [`RequestExt`](../../cot/request/trait.RequestExt.html) in your code most of +//! the time to be able to use these functions: //! //! ``` //! use cot::request::RequestExt; @@ -45,7 +46,7 @@ pub struct RouteName(pub String); /// Path parameters extracted from the request URL, and available as a map of /// strings. /// -/// This struct is meant to be mainly used using the [`PathParams::parse`] +/// This struct is meant to be mainly used via the [`PathParams::parse`] /// method, which will deserialize the path parameters into a type `T` /// implementing `serde::DeserializeOwned`. If needed, you can also access the /// path parameters directly using the [`PathParams::get`] method. diff --git a/cot-core/src/response.rs b/cot-core/src/response.rs index e6233a738..efd244492 100644 --- a/cot-core/src/response.rs +++ b/cot-core/src/response.rs @@ -122,8 +122,9 @@ pub trait ResponseExt: Sized + private::Sealed { /// /// # See also /// - /// * [`crate::reverse_redirect!`] – a more ergonomic way to create - /// redirects to internal views + /// * [`cot::reverse_redirect!`](../../cot/macro.reverse_redirect!.html) – a + /// more ergonomic way to create redirects to internal views (available in + /// the `cot` crate) #[must_use] fn new_redirect>(location: T) -> Self; } diff --git a/cot/src/error.rs b/cot/src/error.rs index ee998cbd3..c92ab450a 100644 --- a/cot/src/error.rs +++ b/cot/src/error.rs @@ -1,3 +1,9 @@ +//! Error handling types and utilities for Cot applications. +//! +//! This module provides error types, error handlers, and utilities for +//! handling various types of errors that can occur in Cot applications, +//! including 404 Not Found errors, uncaught panics, and custom error pages. + pub mod handler; #[doc(inline)] diff --git a/cot/src/error/handler.rs b/cot/src/error/handler.rs index 8dd03002e..9b7a97fc5 100644 --- a/cot/src/error/handler.rs +++ b/cot/src/error/handler.rs @@ -20,7 +20,7 @@ use crate::response::Response; /// This trait is implemented by functions that can handle error pages. The /// trait is automatically implemented for async functions that take parameters /// implementing [`FromRequestHead`] and return a type that implements -/// [`IntoResponse`]. +/// [`IntoResponse`](crate::response::IntoResponse). /// /// # Examples /// diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 3efdd5300..e991c8639 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -55,11 +55,6 @@ pub mod cache; #[cfg(feature = "db")] pub mod db; -/// Error handling types and utilities for Cot applications. -/// -/// This module provides error types, error handlers, and utilities for -/// handling various types of errors that can occur in Cot applications, -/// including 404 Not Found errors, uncaught panics, and custom error pages. pub mod error; pub mod form; // Not public API. Referenced by macro-generated code. diff --git a/cot/src/session/store.rs b/cot/src/session/store.rs index f68ea7ab8..57014b51a 100644 --- a/cot/src/session/store.rs +++ b/cot/src/session/store.rs @@ -26,12 +26,13 @@ pub(crate) const MAX_COLLISION_RETRIES: u32 = 32; pub(crate) const ERROR_PREFIX: &str = "session store:"; /// A wrapper that provides a concrete type for -/// [`tower_session::SessionManagerLayer`] while delegating to a boxed -/// [`SessionStore`] trait object. +/// [`SessionManagerLayer`](tower_sessions::SessionManagerLayer) while +/// delegating to a boxed [`SessionStore`] trait +/// object. /// /// This enables runtime selection of session store implementations, as -/// [`tower_sessions::SessionManagerLayer`] requires a concrete type rather than -/// a boxed trait object. +/// [`SessionManagerLayer`](tower_sessions::SessionManagerLayer) requires a +/// concrete type rather than a boxed trait object. /// /// # Examples ///