diff --git a/Cargo.lock b/Cargo.lock index a4b6edb42..996dbbe66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -876,15 +876,14 @@ dependencies = [ "ahash", "aide", "askama", - "async-stream", "async-trait", "axum", - "backtrace", "bytes", "chrono", "chrono-tz", "chumsky", "clap", + "cot_core", "cot_macros", "criterion", "deadpool-redis", @@ -895,14 +894,12 @@ dependencies = [ "fake", "fantoccini", "form_urlencoded", - "futures", "futures-core", "futures-util", "grass", "hex", "hmac", "http 1.4.0", - "http-body", "http-body-util", "humantime", "idna", @@ -921,15 +918,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", @@ -990,6 +984,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "cot_core" +version = "0.4.0" +dependencies = [ + "askama", + "async-stream", + "axum", + "backtrace", + "bytes", + "cot", + "cot_macros", + "derive_more", + "form_urlencoded", + "futures", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body", + "http-body-util", + "indexmap", + "serde", + "serde_html_form", + "serde_json", + "serde_path_to_error", + "sync_wrapper", + "thiserror 2.0.17", + "tokio", + "tower", + "tower-sessions", +] + [[package]] name = "cot_macros" version = "0.4.0" @@ -3692,9 +3717,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/Cargo.toml b/Cargo.toml index 6840f414b..35a3e83ff 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", @@ -75,6 +76,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..ea60d217e --- /dev/null +++ b/cot-core/Cargo.toml @@ -0,0 +1,49 @@ +[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] +http.workspace = true +derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] } +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 +cot_macros.workspace = true +askama = { workspace = true, features = ["alloc"] } +tower-sessions.workspace = true +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 +cot = { workspace = true, features = ["test"] } +futures.workspace = true +tokio.workspace = true + +[features] +default = [] +json = [] diff --git a/cot/src/body.rs b/cot-core/src/body.rs similarity index 96% rename from cot/src/body.rs rename to cot-core/src/body.rs index 874307c30..bd05b3351 100644 --- a/cot/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; @@ -9,7 +14,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. @@ -28,10 +33,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 +171,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 +266,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/error.rs b/cot-core/src/error.rs new file mode 100644 index 000000000..d73e50c24 --- /dev/null +++ b/cot-core/src/error.rs @@ -0,0 +1,16 @@ +//! 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; +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..e06f1d818 100644 --- a/cot/src/error/backtrace.rs +++ b/cot-core/src/error/backtrace.rs @@ -1,7 +1,8 @@ // 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 { +#[must_use] +pub fn __cot_create_backtrace() -> Backtrace { let mut backtrace = Vec::new(); let mut start = false; backtrace::trace(|frame| { @@ -21,19 +22,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 +43,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 +51,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/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/src/handler.rs b/cot-core/src/handler.rs similarity index 91% rename from cot/src/handler.rs rename to cot-core/src/handler.rs index be8f6423c..1b24a7162 100644 --- a/cot/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; @@ -48,14 +54,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>); @@ -142,6 +148,7 @@ macro_rules! impl_request_handler_from_request { }; } +#[macro_export] macro_rules! handle_all_parameters { ($name:ident) => { $name!(); @@ -227,19 +234,21 @@ 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); +#[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 new file mode 100644 index 000000000..52458b055 --- /dev/null +++ b/cot-core/src/headers.rs @@ -0,0 +1,11 @@ +//! 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"; +#[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/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/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 new file mode 100644 index 000000000..bd0e82093 --- /dev/null +++ b/cot-core/src/lib.rs @@ -0,0 +1,34 @@ +//! 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; +#[macro_use] +pub mod handler; +pub mod headers; +pub mod html; +#[cfg(feature = "json")] +pub mod json; +pub mod middleware; +pub mod request; +pub mod response; + +pub use body::{Body, BodyInner}; +pub use error::Error; + +/// 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; + +/// A type alias for a result that can return a [`Error`]. +pub type Result = std::result::Result; diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs new file mode 100644 index 000000000..0a1bdbdc5 --- /dev/null +++ b/cot-core/src/middleware.rs @@ -0,0 +1,246 @@ +//! 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}; + +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}; + +#[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/struct.RootHandlerBuilder.html#method.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 [`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)))) +} + +#[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/struct.RootHandlerBuilder.html#method.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 [`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-core/src/request.rs b/cot-core/src/request.rs new file mode 100644 index 000000000..c6ada6fa9 --- /dev/null +++ b/cot-core/src/request.rs @@ -0,0 +1,327 @@ +//! 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`](../../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; +//! ``` + +use indexmap::IndexMap; + +use crate::Body; +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 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. +/// +/// # 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, + ) -> 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..0bbb73fb0 --- /dev/null +++ b/cot-core/src/request/extractors.rs @@ -0,0 +1,502 @@ +//! 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::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/response.rs b/cot-core/src/response.rs similarity index 96% rename from cot/src/response.rs rename to cot-core/src/response.rs index e6233a738..efd244492 100644 --- a/cot/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/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 1f1d1f909..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::error_impl::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/Cargo.toml b/cot/Cargo.toml index e704c97d6..8b9a57976 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -20,12 +20,12 @@ 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 chumsky = { workspace = true, optional = 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 @@ -39,7 +39,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 idna = { workspace = true, optional = true } @@ -55,14 +54,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"] } @@ -74,11 +70,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 @@ -118,7 +112,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/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/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 fbe866592..d779ec1f8 100644 --- a/cot/src/config.rs +++ b/cot/src/config.rs @@ -27,7 +27,7 @@ use thiserror::Error; #[cfg(feature = "email")] use crate::email::transport::smtp::Mechanism; -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/email.rs b/cot/src/email.rs index 8bd6855ad..6d278aada 100644 --- a/cot/src/email.rs +++ b/cot/src/email.rs @@ -39,7 +39,7 @@ use transport::{BoxedTransport, Transport}; use crate::email::transport::TransportError; use crate::email::transport::console::Console; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; const ERROR_PREFIX: &str = "email message build error:"; /// Represents errors that can occur when sending an email. diff --git a/cot/src/email/transport.rs b/cot/src/email/transport.rs index f5d75c700..30043b28f 100644 --- a/cot/src/email/transport.rs +++ b/cot/src/email/transport.rs @@ -11,7 +11,7 @@ use cot::email::EmailMessageError; use thiserror::Error; use crate::email::EmailMessage; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; pub mod console; pub mod smtp; diff --git a/cot/src/error.rs b/cot/src/error.rs index 2e24fa372..c92ab450a 100644 --- a/cot/src/error.rs +++ b/cot/src/error.rs @@ -1,10 +1,12 @@ -pub(crate) mod backtrace; -pub(crate) mod error_impl; +//! 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; -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/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/error_page.rs b/cot/src/error_page.rs index 71629d2ef..1cd510680 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -73,14 +73,14 @@ 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::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/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 9776dc418..febb48a6f 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -55,38 +55,27 @@ 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; -mod headers; // Not public API. Referenced by macro-generated code. #[doc(hidden)] #[path = "private.rs"] pub mod __private; pub mod admin; pub mod auth; -mod body; pub mod cli; pub mod common_types; pub mod config; #[cfg(feature = "email")] pub mod email; mod error_page; -#[macro_use] -pub(crate) mod handler; -pub mod html; -#[cfg(feature = "json")] -pub mod json; +#[doc(inline)] +pub(crate) use cot_core::handler; pub mod middleware; #[cfg(feature = "openapi")] pub mod openapi; pub mod project; pub mod request; -pub mod response; pub mod router; mod serializers; pub mod session; @@ -97,7 +86,13 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; -pub use body::Body; +#[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, response}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// @@ -161,7 +156,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}; @@ -170,12 +164,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; - -/// 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; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index b9e6e27cc..eb91f7f3c 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -9,15 +9,12 @@ 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}; +use crate::Error; #[cfg(feature = "cache")] use crate::config::CacheType; use crate::config::{Expiry, SameSite, SessionStoreTypeConfig}; @@ -32,240 +29,17 @@ 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; +#[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. 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}; diff --git a/cot/src/project.rs b/cot/src/project.rs index beaaa235c..5fcaeed95 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -53,9 +53,8 @@ use crate::db::Database; use crate::db::migrations::{MigrationEngine, SyncDynMigration}; #[cfg(feature = "email")] use crate::email::Email; -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 6098ef34b..7dd42b282 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -15,27 +15,21 @@ 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; +use crate::Result; #[cfg(feature = "db")] use crate::db::Database; #[cfg(feature = "email")] use crate::email::Email; -use crate::error::error_impl::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 {} } @@ -421,308 +415,15 @@ 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::*; + use crate::Body; use crate::request::extractors::Path; use crate::response::Response; 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 51672e4de..c2c759629 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -48,64 +48,18 @@ //! # } //! ``` -use std::future::Future; use std::sync::Arc; -use serde::de::DeserializeOwned; +#[doc(inline)] +pub use cot_core::request::extractors::*; +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 +67,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 +239,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 +257,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::error_impl::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 +320,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( 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/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 /// 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 20294b9a6..ee24056ce 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -42,7 +42,7 @@ use crate::email::Email; #[cfg(feature = "email")] use crate::email::transport::console::Console; #[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;